Convert JSON to case class with a nested objects using Scala/Play - json

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.

Related

Dynamic Json values in Play Framework Json

I am currently using the Play framework json parser in order to parse a json string in my scala code.
I have the following class:
case class Address(address: String,
gps: GPS,
country: String) {}
object Address {
implicit val reads: Reads[Address] = (
(JsPath \ "address").read[String] and
(JsPath \ "gps").read[GPS] and
(JsPath \ "country").read[String]
) (Address.apply _)
implicit val writes: Writes[Address] = (
(JsPath \ "address").write[String] and
(JsPath \ "gps").write[GPS] and
(JsPath \ "country").write[String]
) (unlift(Address.unapply))
}
Which works fine with the following json:
{
"address": "123 Fake Street",
"country": "USA",
"gps": { ... }
}
The problem is that in some situations the json may instead have the gps field be a string which doesnt parse, i.e.
{
"address": "123 Fake Street",
"country": "USA",
"gps": "123abc"
}
Now I know that I cant have the gps member be both a string or a GPS object, but is there any way to have it be say an Option[GPS] and only have a value if the json contained a gps object?
Only very little is needed to be changed in your impl.
You need to read the field "gps" as something that is 'safe' like JsValue and then try to map it into your GPS case class if it can be done, if not, return None.
case class GPS(a:String, b:String)
object GPS {
val travelInfoReads = Json.reads[GPS]
val travelInfoWrites = Json.writes[GPS]
implicit val travelInfoFormat: Format[GPS] = Format(travelInfoReads, travelInfoWrites)
}
case class Address(address: String,
gps: Option[GPS],
country: String) {}
object Address {
implicit val reads: Reads[Address] = (
(JsPath \ "address").read[String] and
(JsPath \ "gps").read[JsValue].map(js => js.asOpt[GPS]) and
(JsPath \ "country").read[String]
) (Address.apply _)
implicit val writes: Writes[Address] = (
(JsPath \ "address").write[String] and
(JsPath \ "gps").writeNullable[GPS] and
(JsPath \ "country").write[String]
) (unlift(Address.unapply))
}
I also tested it:
val json = Json.toJson(Address("1",Some(GPS("a","b")),"2"))
println(json)
println(json.as[Address])
val newObj: JsObject = (json.as[JsObject] - "gps") + ("gps" -> JsNumber(1))
println(newObj)
val a = newObj.as[Address]
println(a)
a must beEqualTo(Address("1",None,"2"))
Output was like
{"address":"1","gps":{"a":"a","b":"b"},"country":"2"}
Address(1,Some(GPS(a,b)),2)
{"address":"1","country":"2","gps":1}
Address(1,None,2)

play json in scala: deserializing json with unknown fields without losing them

consider i have a json as following:
{
"a": "aa",
"b": "bb",
"c": "cc",
"d": "dd", // unknown in advance
"e": { //unknown in advance
"aa": "aa"
}
}
i know for sure that the json will contain a,b,c but i've no idea what other fields this json may contain.
i want to serialize this JSON into a case class containing a,b,c but on the other hand not to lose the other fields (save them in a map so the class will be deserialized to the same json as received).
ideas?
One option is to capture the "unknown" fields in a Map[String,JsValue], from which you can later extract values if you need them.
case class MyClass(a: String, b: String, c: String, extra: Map[String, JsValue])
implicit val reads: Reads[MyClass] = (
(__ \ "a").read[String] and
(__ \ "b").read[String] and
(__ \ "c").read[String] and
__.read[Map[String, JsValue]]
.map(_.filterKeys(k => !Seq("a", "b", "c").contains(k)))
)(MyClass.apply _)
// Result:
// MyClass(aa,bb,cc,Map(e -> {"aa":"aa"}, d -> "dd"))
Likewise, you can do a Writes or a Format like so:
// And a writes...
implicit val writes: Writes[MyClass] = (
(__ \ "a").write[String] and
(__ \ "b").write[String] and
(__ \ "c").write[String] and
__.write[Map[String, JsValue]]
)(unlift(MyClass.unapply _))
// Or combine the two...
implicit val format: Format[MyClass] = (
(__ \ "a").format[String] and
(__ \ "b").format[String] and
(__ \ "c").format[String] and
__.format[Map[String, JsValue]](Reads
.map[JsValue].map(_.filterKeys(k => !Seq("a", "b", "c").contains(k))))
)(MyClass.apply, unlift(MyClass.unapply))
Note: it looks a bit confusing because you give the format for Map[String,JsValue] an explicit Reads as an argument (Reads.map), which you then transform (using the .map method) to remove the already-captures values.
You can use a custom Reads for this, something like:
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class MyData(a: String, b: String, c:String, other: Map[String, JsValue])
object MyData {
val abcReader: Reads[(String, String, String)] = (
(JsPath \ "a").read[String] and
(JsPath \ "b").read[String] and
(JsPath \ "c").read[String]
).tupled
implicit val reader: Reads[MyData] = Reads { json =>
abcReader.reads(json).map {
case (a, b, c) =>
val other = json.as[JsObject].value -- Seq("a", "b", "c")
MyData(a, b, c, other.toMap)
}
}
}

Playframework - JSON parsing object with single field - definition issue

I cannot find a way how to make it work when deserialized object has single field - I cannot compile the code. Seems that and operator does some transformation and I cannot find a method to call to do the same.
I have following json:
{"total": 53, "max_score": 3.2948244, "hits": [
{
"_index": "h",
"_type": "B",
"_id": "3413569628",
"_score": 3.2948244,
"_source": {
"fotky": [
{
"popisek":" ",
"localFileSystemLocation":" ",
"isMain": true,
"originalLocation": ""
}
]
}
}
]
}
I try the following data model to de serialize to:
case class SearchLikeThisResult(total: Int, max_score: Double, hits: Seq[Hits])
case class Hits(_index: String, _type: String, _id: String, _score: Double, _source: Source)
case class Source(fotky: Seq[Photo])
case class Photo(isMain: Boolean, originalLocation: Option[String], localFileSystemLocation: Option[String], popisek: Option[String])
Implicit reads as follows:
object SearchLikeThisHits {
import play.api.libs.functional.syntax._
implicit val photoReads: Reads[Photo] = (
(JsPath \ "isMain").read[Boolean] and
(JsPath \ "originalLocation").readNullable[String] and
(JsPath \ "localFileSystemLocation").readNullable[String] and
(JsPath \ "popisek").readNullable[String]
)(Photo.apply _)
implicit val sourceReads: Reads[Source] = (
(JsPath \ "fotky").read[Seq[Photo]]
)(Source.apply _)
implicit val hitsReads: Reads[Hits] = (
(JsPath \ "_index").read[String] and
(JsPath \ "_type").read[String] and
(JsPath \ "_id").read[String] and
(JsPath \ "_score").read[Double] and
(JsPath \ "_source").read[Source]
)(Hits.apply _)
implicit val searchLikeThisResult: Reads[SearchLikeThisResult] = (
(JsPath \ "total").read[Int] and
(JsPath \ "max_score").read[Double] and
(JsPath \ "hits").read[Seq[Hits]]
)(SearchLikeThisResult.apply _)
}
What I am really struggling with is under the _source:
implicit val sourceReads: Reads[Source] = (
(JsPath \ "fotky").read[Seq[Photo]]
)(Source.apply _)
where read is reported as unkown symbol - in other cases and performs some transformation.
Inline definition doesn't help either.
Does anybody faced this before?
The fancy applicative builder syntax (and, etc.) is nice, but it can obscure the fact that Reads is monadic and also works perfectly well with map, flatMap, for-comprehensions, etc.
So while the applicative builder syntax doesn't work with single values, plain old map does:
implicit val sourceReads: Reads[Source] =
(JsPath \ "fotky").read[Seq[Photo]].map(Source(_))
The key here is that (JsPath \ "fotky").read[Seq[Photo]] is a Reads[Seq[Photo]], and you want a Reads[Source]. map gives you a way to get from one to the other, just as you could use it to transform an Option[Seq[Photo]] into an Option[Source, for example.
You could save yourself some trouble by making use of the Json.reads to automatically generate your Reads (provided the case class is defined exactly like the Json objects - which is your case).
implicit val photoReads = Json.reads[Photo]
implicit val sourceReads = Json.reads[Source]
implicit val hitsReads = Json.reads[Hits]
implicit val searchResultReads = Json.reads[SearchLikeThisResult]
For more information, see https://www.playframework.com/documentation/2.1.1/ScalaJsonInception

setting default values with Play! Json Combinators

I'm using play!'s json combinators to validate, read and write JSON. Is it possible to specify default values in reads or writes if they are not set?
validation of json is done like this (where json is a JsValue):
json.validate[Pricing]
My code is:
case class Pricing(
_id: ObjectId = new ObjectId,
description: String,
timeUnit: TimeUnit.Value,
amount: Double = 0.0) {
#Persist val _version = 1
}
my reads and writes:
implicit val pricingReads: Reads[Pricing] = (
(__ \ "_id").read[ObjectId] and
(__ \ "description").read[String] and
(__ \ "timeUnit").read[TimeUnit.Value] and
(__ \ "amount").read[Double]
)(Pricing.apply _)
implicit val pricingWrites: Writes[Pricing] = (
(__ \ "_id").write[ObjectId] and
(__ \ "description").write[String] and
(__ \ "timeUnit").write[TimeUnit.Value] and
(__ \ "amount").write[Double]
)(unlift(Pricing.unapply))
so if I would recieve a Json like:
{"description": "some text", "timeUnit": "MONTH"}
I get errors, that fields _id and amount are missing. Is there any possiblity to set the default values without adding it directy to the JsValue?
Thanks in advance!
I'd rather use Options:
case class Pricing(
_id: Option[ObjectId],
description: String,
timeUnit: TimeUnit.Value,
amount: Option[Double]) {
#Persist val _version = 1
}
and replace your pricingReads with this:
implicit val pricingReads: Reads[Pricing] = (
(__ \ "_id").readNullable[ObjectId] and
(__ \ "description").read[String] and
(__ \ "timeUnit").read[TimeUnit.Value] and
(__ \ "amount").readNullable[Double]
)(Pricing.apply _)
Then your code will work on missing fields and yo will be able to do this:
_id.getOrElse(new ObjectId)

Scala object to Json Formatter using combinators

I have implemented a class following Scala documentation
case class Creature(
name: String,
isDead: Boolean,
weight: Float,
dob: java.sql.Date
)
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val creatureFormat = (
(__ \ "name").format[String] and
(__ \ "isDead").format[Boolean] and
(__ \ "weight").format[Float] and
(__ \ "dob").format[java.sql.Date]
)(Creature.apply, unlift(Creature.unapply))
Then I call the json wrapper like this Json.toJson(Creature("John Doe", false, 100.0, new java.sql.Date(1363456800000))) and expect to see an output like {"name": "John Doe", "isDead": false, "weight": 100.0, "dob": "2013-03-17"}. Instead, I am getting an output like {"name": "John Doe", "isDead": false, "weight": 100.0, "dob": 1363456800000}.
Please note that, in the database, I can see the dob as 2013-03-17.
By default the java.util.Date Json serializer produces a number containing the date timestamp.
Alternatively, you can use a date serializer that produces a String containing a representation of the date. However, because there is no standard representation of dates in JSON, you have to explicitly supply the pattern to use to produce the text representation:
implicit val creatureFormat = (
(__ \ "name").format[String] and
(__ \ "isDead").format[Boolean] and
(__ \ "weight").format[Float] and
(__ \ "dob").format(sqlDateWrites("YYYY-MM-DD"))(sqlDateReads("YYYY-MM-DD"))
)(Creature.apply, unlift(Creature.unapply))
Here's how I resolved it (I explicitly defined apply and unapply methods)
val sdf = new java.text.SimpleDateFormat("yyyy-MM-dd")
implicit val creatureFormat = (
(__ \ "name").format[String] and
(__ \ "isDead").format[Boolean] and
(__ \ "weight").format[Float] and
(__ \ "dob").format[String])
(((name, isDead, weight, dob) => Creature(name, isDead, weight, new java.sql.Date(sdf.parse(dob).getTime()))),
unlift((cr: Creature) => Some(cr.name, cr.isDead, cr.weight, sdf.format(cr.dob))))
I do not know whether there is any better solutions.
Update
Finally, I implemented a formatter for java.sql.Date
import play.api.libs.json._
import play.api.libs.functional.syntax._
import play.api.data.validation.ValidationError
import play.api.libs.json.{ Json => PlayJson, _ }
case class Creature(
name: String,
isDead: Boolean,
weight: Float,
dob: java.sql.Date
)
implicit val sqlDateWrite = new Format[SqlDate] {
def reads(json: JsValue) = json match {
case JsString(d) => {
val theDate = new SqlDate(sdf.parse(d).getTime)
if (d.matches(sdfPattern) && theDate.compareTo(new Date(0)) > 0) JsSuccess(new SqlDate(sdf.parse(d).getTime))
else JsError(Seq(JsPath() -> Seq(ValidationError("validate.error.expected.date.in.format(dd-MM-yyyy)"))))
}
case _ => JsError(Seq(JsPath() -> Seq(ValidationError("validate.error.expected.date.in.String"))))
}
def writes(sd: SqlDate): JsValue = JsString(sdf.format(sd))
}
implicit val creatureFormat = PlayJson.format[Creature]
Now, both these lines works
val mcJson = PlayJson.toJson(Creature("John Doe", false, 100, new SqlDate(1368430000000L)))
val mcObj = PlayJson.fromJson[Creature](PlayJson.obj("name"-> "Abul Khan", "isDead"-> true, "weight"-> 115, "dob"-> "17-05-2011")).getOrElse(null)
Since you expect strings you'd have to convert everything to string and lose typing.
Json.toJson(
Creature(
"John Doe", "false", "100.0",(new java.sql.Date(1363456800000)).toString
)
)