alapeno
alapeno

Reputation: 2794

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?

Upvotes: 1

Views: 1507

Answers (2)

Travis Brown
Travis Brown

Reputation: 139048

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.

Upvotes: 2

Tyth
Tyth

Reputation: 1824

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

Upvotes: 1

Related Questions