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

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

Related

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

How do you add validation to a Reads in Play Json?

Let's say I've got a reads that creates an object from JSON with two optional fields:
implicit val rd: Reads[MyObject] = (
(__ \ "field1").readNullable[String] and
(__ \ "field2").readNullable[String]
)(MyObject.apply _)
I want to check to make sure that the value of field1 is one of the values in the list:
List("foo", "bar")
I can do that after the fact, by creating a new MyObject and mapping the values through a function to transform them, but I feel like there should be a way to do this more elegantly using JSON transformers or something.
Ideally, I want the Reads to read the nullable value of field1 and transform it if it is defined, without the need to post-process it. Is there some way of sneaking a transform in there?
You can use this approach:
case class MyObject(a: Option[String], b: Option[String])
val allowedValues = Seq("foo", "bar")
implicit val reads: Reads[MyObject] = new Reads[MyObject] {
override def reads(json: JsValue): JsResult[MyObject] = {
val a = (json \ "a").asOpt[String].filter(allowedValues.contains)
val b = (json \ "b").asOpt[String]
JsSuccess(MyObject(a, b))
}
}
Usage examples:
scala> Json.parse(""" { "a": "bar", "b": "whatever"} """).validate[MyObject]
res2: play.api.libs.json.JsResult[MyObject] = JsSuccess(MyObject(Some(bar),Some(whatever)),)
scala> Json.parse(""" { "a": "other", "b": "whatever"} """).validate[MyObject]
res3: play.api.libs.json.JsResult[MyObject] = JsSuccess(MyObject(None,Some(whatever)),)
scala> Json.parse(""" {} """).validate[MyObject]
res4: play.api.libs.json.JsResult[MyObject] = JsSuccess(MyObject(None,None),)
Okay, after doing some more research, I came up with the following:
In play.api.libs.json.ConstraintReads there is a function called verifying(cond: A => Boolean) that returns a Reads[A]. This can be passed as a parameter to JsPath.readNullable[A] like so:
implicit val rd: Reads[MyObject] = (
(__ \ "field1").readNullable[String](verifying(allowedValues.contains)) and
(__ \ "field2").readNullable[String]
)(MyObject.apply _)
This will return a JsResponse, either a JsSuccess if "field1" validates, or a JsError if it doesn't validate. It actually fails on an invalid input, rather than just ignoring the input. That's more like the behaviour I wanted.
There are a number of other constraint functions that perform similar tests on the read value, as well.

PlayFramework in Scala, Reads - error in case of unknown key? [duplicate]

Play's JSON serialization is by default permissive when serializing from JSON into a case class. For example.
case class Stuff(name: String, value: Option[Boolean])
implicit val stuffReads: Reads[Stuff] = (
( __ \ 'name).read[String] and
( __ \ 'value).readNullable[Boolean]
)(Stuff.apply _)
If the following JSON was received:
{name: "My Stuff", value: true, extraField: "this shouldn't be here"}
It will succeed with a 'JsSuccess' and discard the 'extraField'.
Is there a way to construct the Json Reads function to have it return a JsError if there are 'unhandled' fields?
You can verify that the object doesn't contain extra keys before performing your own decoding:
import play.api.data.validation.ValidationError
def onlyFields(allowed: String*): Reads[JsObject] = Reads.filter(
ValidationError("One or more extra fields!")
)(_.keys.forall(allowed.contains))
Or if you don't care about error messages (and that one's not very helpful, anyway):
def onlyFields(allowed: String*): Reads[JsObject] =
Reads.verifying(_.keys.forall(allowed.contains))
And then:
implicit val stuffReads: Reads[Stuff] = onlyFields("name", "value") andThen (
(__ \ 'name).read[String] and
(__ \ 'value).readNullable[Boolean]
)(Stuff)
The repetition isn't very nice, but it works.
Inspired from Travis' comment to use LabelledGeneric I was able achieve compile time safe solution.
object toStringName extends Poly1 {
implicit def keyToStrName[A] = at[Symbol with A](_.name)
}
case class Foo(bar: String, boo: Boolean)
val labl = LabelledGeneric[Foo]
val keys = Keys[labl.Repr].apply
now keys.map (toStringName).toList will give you
res0: List[String] = List(bar, boo)

If statements within Play/Scala JSON parsing?

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

Parsing a List of Models in Play! 2.1.x

Given the JSON...
[ {"ID": "foo"}, {"ID": "bar"} ]
Represented with case classes...
case class Example(models: List[Model])
case class Model(id: String)
I attempt the following which fails with overloaded method value read with alternatives.
trait JsonReader {
implicit val modelReads: Reads[Model] = (__ \ "name").read[String](Model)
implicit val exampleReads: Reads[Example] = JsPath.read[List[Model]](Example)
def get (response: Response) = response.json.as[Example]
}
What is the correct way to parse this?
For a strange reason I did not find an elegant solution to read a json model with only one value. For 2 and more values you may write:
implicit val reader = (
(__ \ 'id).read[Long] and
(__ \ 'field1).read[String] and
(__ \ 'field2).read[String])(YourModel.apply _)
For a json with 1 field try using something like that:
implicit val reader = new Reads[Model] {
def reads(js: JsValue): JsResult[Model] = {
JsSuccess(Model((js \ "name").as[String]))
}
}
This should work but doesn't look nice :(