问题
I have the following class:
case class Model(elements: List[(String, String)])
Now I want to fill my model Model
with the values of a JsValue
by using Reads[T]
. The JSON could have different key values pairs which are unknown at the time of unmarshaling them and therefore I want to have them as a list of tuples.
For example:
{ "foo": "bar", "barfoo": "foobar"}
Should become:
List(("foo" -> "bar"), ("barfoo" -> "foobar"))
The problem is that I don't know how I can achieve a sort of wildcard function that matches all elements in a JSON object, but not nested ones or arrays.
implicit val modelReads: Reads[Model] = (
(JsPath \ "?").read[String] // and
// (JsPath \ "foo").read[String] // and <- key not known in advance
// (JsPath \ "barfoo").read[String] // <- key not known in advance
) (Model.apply _)
回答1:
You won't be able to use Play JSON combinators for everything here, as they only work with fixed field mappings. For you to be able to read the elements
field, you would need to implement a Reads[List[(String, String)]]
. Fortunately, Play already has a Reads[Map[A, B]]
available (for types A
and B
that also have a Reads
), and a Map[A, B]
can easily be converted into a List[(A, B)]
(underneath a Map
is just a collection of tuples).
For a one-off case, we can use read[Map[String, String]]
and map
it to a List
. Then, we can map that to the case class. Assuming the following JSON structure:
val js = Json.parse("""{"element": { "foo": "bar", "barfoo": "foobar"}}""")
You can write:
implicit val reads = (__ \ "elements").read[Map[String, String]]
.map(_.toList)
.map(tuples => Model(tuples))
And try it out:
scala> js.validate[Model]
res8: play.api.libs.json.JsResult[Model] = JsSuccess(Model(List((foo,bar), (barfoo,foobar))),/elements)
Note that the Reads[Model]
above is kind of a special case, because the case class only had a single field. To take this a bit further and see how it can play with JSON combinators, let's add a new field:
case class Model(elements: List[(String, String)], info: String)
Then, let's also make our Reads
for the tuples a little more generic, so that it can handle values of any type A
where a Reads[A]
is available:
implicit def tupleReads[A](implicit rds: Reads[A]): Reads[List[(String, A)]] =
Reads.mapReads(rds).map(_.toList)
Now we can write a Reads
using combinators for the newly defined Model
, the same as you're used to:
implicit val reads = (
(__ \ "elements").read[List[(String, String)]] and
(__ \ "info").read[String]
)(Model.apply _)
Trying it out:
val js = Json.parse("""{"elements": { "foo": "bar", "barfoo": "foobar"}, "info": "test"}""")
scala> js.validate[Model]
res0: play.api.libs.json.JsResult[Model] = JsSuccess(Model(List((foo,bar), (barfoo,foobar)),test),)
If your JSON structure only looks like {"foo": "bar", "barfoo": "foobar"}
(without an elements
key), then we can still leverage the same generic Reads[List[(String, A)]]
, but will need to implement a more custom Reads[Model]
to map an entire object to one model field. Let's we want to map the above JSON to:
Model(List(("foo" -> "bar"), ("barfoo" -> "foobar")))
The Reads[Model]
we need will basically be the same as the first one I defined, except that we can drop the JsPath
from it:
// Use `tupleReads` as defined above, restricted to `String`
implicit val reads = tupleReads[String].map(tuples => Model(tuples))
It works:
val js = Json.parse("""{"foo": "bar", "barfoo": "foobar"}""")
scala> js.validate[Model]
res0: play.api.libs.json.JsResult[Model] = JsSuccess(Model(List((foo,bar), (barfoo,foobar))),)
回答2:
Here is the draft code:
val json = Json.parse("""
{ "foo": "bar", "barfoo": "foobar"}
""")
implicit val readMetaTag =
Reads(js => JsSuccess(
Model(js.as[JsObject].fieldSet.map(
tag => (tag._1, tag._2.as[String])).toList)))
val model = json.as[Model]
println("Model: " + model)
//Model: Model(List((foo,bar), (barfoo,foobar)))
来源:https://stackoverflow.com/questions/42753388/convert-a-jsvalue-to-a-model-via-readst-which-consists-of-a-list-of-tuples