Scala object to Json Formatter using combinators - json

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

Related

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

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.

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

Can I write Json from two model without transformers in Play Framework 2.4

I have two model classes
case class Calendar(id: String, summary: String)
case class ACL(account: String, role: String)
and i want write Json
{
"id": "some id",
"summary": "some text",
"acl": [
"user": "some user",
"role": "some role"
]
}
without Json transformers.
now I have
val calendar = ...
val acl = ...
val calendarWrite = (
(__ \ "_id").write[String] and
(__ \ "summary").write[String]
)(unlift(Calendar.unapply))
val aclWrite = (
(__ \ "user").write[String] and
(__ \ "role").write[String]
)(unlift(ACL.unapply))
val updateForMongo =
__.json.update(
(__ \ "acl" ).json.put(
JsArray( Seq( aclWrite.writes(acl) ))
)
)
calendarWrite.writes(calendar)
.transform(updateForMongo)
.fold(
invalid =>
Future.successful(0),
valid =>
calendarsCollection.insert(valid).map(l => l.n)
)
Is there a possibility to write into the write stream multiple objects?
And what about "one field" model class? Can i write custom Write, are there any workarounds?
it's a simple question if you forget about all sorts of transformers and more common terms and see in the JsObject source.
++, -, +, deepMerge that's all you need.
calendarWrite.writes(calendar) + ("acl" -> JsArray(Seq( aclWrite.writes(acl) )))
And for "one field" model class:
case class NotificationSettings(notifications: Seq[Notification])
val nwrite = new Writes[NotificationSettings]{
override def writes(o: NotificationSettings): JsValue = {
Json.obj("notifications" -> o.notifications)
}
}

play-json: how to get value of "dynamic" key using Reads[T]?

I have a Seq of JsValue elements. Each element represents the following JSON structure with two fields:
{
"name": "xy"
"key ∈ {A,B,C}": ["// some values in an array"]
}
What this means is that I know the key of the first field (always "name"), but not the key of the array since it is "dynamic". But: the possible keys are known, it is either "A", "B" or "C".
What I want to do is to map each of these JsValue objects to a case class:
case class Element(name: String, values: Seq[String])
As you can see, the name of the dynamic key is not even important. I just want to get the array that is associated with it.
But: how can I fetch the array with Reads[T] if its key differs?
implicit val reads: Reads[Element] = (
(__ \ "name").read[String] and
(__ \ "???").read[Seq[String]]
)(Element.apply _)
Or does this have to be done "manually", if yes, how?
As the other answer notes, orElse works here, but if you want more flexibility you can always write something like a method that returns a Reads that looks for a key that satisfies some predicate:
import play.api.libs.json._
def findByKey[A: Reads](p: String => Boolean): Reads[A] = Reads[A] {
case JsObject(fields) => fields.find(kv => p(kv._1)).map(
_._2.validate[A]
).getOrElse(JsError("No valid field key"))
case _ => JsError("Not an object")
}
And then:
import play.api.libs.functional.syntax._
case class Element(name: String, values: Seq[String])
object Element {
implicit val reads: Reads[Element] = (
(__ \ "name").read[String] and findByKey[Seq[String]](Set("A", "B", "C"))
)(Element.apply _)
}
And finally:
scala> Json.parse("""{ "name": "foo", "A": ["bar", "baz"] }""").asOpt[Element]
res0: Option[Element] = Some(Element(foo,List(bar, baz)))
scala> Json.parse("""{ "name": "foo", "A": [1, 2] }""").asOpt[Element]
res1: Option[Element] = None
Which approach you choose is a matter of taste, and will probably depend in part on whether the more general findByKey is useful to you in other contexts.
You can use orElse method
case class Element(name: String, values: Seq[String])
object Element {
implicit val reads: Reads[Element] = (
(__ \ "name").read[String] and
(__ \ "a").read[Seq[String]]
.orElse((__ \ "b").read[Seq[String]])
.orElse((__ \ "c").read[Seq[String]])
)(Element.apply _)
}

Creating Writes[T] for Custom Object

Play for Scala shows how to convert JSON to a Scala object.
case class Product(ean: Long, name: String, description: String)
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val productWrites: Writes[Product] = (
(JsPath \ "ean").write[Long] and
(JsPath \ "name").write[String] and
(JsPath \ "description").write[String]
)(unlift(Product.unapply))
And then using in REPL:
scala> val p = Product(100, "tilley hat", "Nice hat")
p: Product = Product(100,tilley hat,Nice hat)
scala> Json.toJson(p)
res1: play.api.libs.json.JsValue = {"ean":100,"name":"tilley hat",
"description":"Nice hat"}
What's going on with the last line: (unlift(Product.unapply)) of the Writes[Product]?
Product.unapply _ is a function Product => Option[(Long, String, String)].
Result type of this expression:
(
(JsPath \ "ean").write[Long] and
(JsPath \ "name").write[String] and
(JsPath \ "description").write[String]
)
is FunctionalBuilder[OWrites]#CanBuild3[Long,String,String]. It accepts T => (Long, String, String) as parameter of method apply.
So you have to convert Product => Option[(Long, String, String)] to Product => (Long, String, String).
Method unlift accepts T => Option[R] and returns T => R. Unlifted function throws MatchError instead of None. It produces something like this:
val unlifted = (Product.unapply _) andThen { case Some(r) => r }
Default unapply method for case class should never return None, so for case class unlift is safe.