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

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

Related

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

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

Custom Json Writes with combinators - not all the fields of the case class are needed

I'm trying to write a custom Json serializer in play for a case class but I don't want it to serialize all the fields of the class. I'm pretty new to Scala, so that is surely the problem but this is what I tried so far:
case class Foo(a: String, b: Int, c: Double)
Now the default way of doing this, as far as I saw in the examples is:
implicit val fooWrites: Writes[Foo] = (
(__ \ "a").write[String] and
(__ \ "b").write[Int]
(__ \ "c").write[Double]
) (unlift(Foo.unapply))
But what if I want to omit "c" from the Json output? I've tried this so far but it doesn't compile:
implicit val fooWritesAlt: Writes[Foo] = (
(__ \ "a").write[String] and
(__ \ "b").write[Int]
) (unlift({(f: Foo) => Some((f.a, f.b))}))
Any help is greatly appreciated!
If you are using Playframework 2.2 (not sure about earlier versions, but it should work as well) try this:
implicit val writer = new Writes[Foo] {
def writes(foo: Foo): JsValue = {
Json.obj("a" -> foo.a,
"b" -> foo.b)
}
}
What I usually do is convert a field to None and use writeNullable on it:
implicit val fooWrites: Writes[Foo] = (
(__ \ "a").write[String] and
(__ \ "b").write[Int]
(__ \ "c").writeNullable[Double].contramap((_: Double) => None)
) (unlift(Foo.unapply))
Instead of specifying a combinator which produces an OWrites instance, one can directly construct an OWrites just as well:
val ignore = OWrites[Any](_ => Json.obj())
implicit val fooWrites: Writes[Foo] = (
(__ \ "a").write[String] and
(__ \ "b").write[Int] and
ignore
) (unlift(Foo.unapply))
This will ignore any value of the case class at that position and simply always return an empty JSON object.
This is very easy to do with the Play's JSON transformers:
val outputFoo = (__ \ 'c).json.prune
and then apply transform(outputFoo) to your existing JsValue:
val foo: Foo
val unprunedJson: JsValue = Json.toJson(foo)
unprunedJson.transform(outputFoo).map { jsonp =>
Ok(Json.obj("foo" -> jsonp))
}.recoverTotal { e =>
BadRequest(JsError.toFlatJson(e))
}
see here http://mandubian.com/2013/01/13/JSON-Coast-to-Coast/
What version of Play are you using? fooWritesAlt compiles for me using 2.1.3. One thing to note is that I needed to explicitly use the writes object to write the partial JSON object, i.e.
fooWritesAt.writes(Foo("a", 1, 2.0))
returns
{"a": "a", "b": 2}

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