通过由元组列表组成的 Reads[T] 将 JsValue 转换为模型

Convert a JsValue to a model via Reads[T] which consists of a list of tuples

我有以下 class:

case class Model(elements: List[(String, String)])

现在我想使用 Reads[T]JsValue 的值填充我的模型 Model。 JSON 可能有不同的键值对,这些键值对在解组时是未知的,因此我想将它们作为元组列表。

例如:

{ "foo": "bar", "barfoo": "foobar"}

应该变成:

List(("foo" -> "bar"), ("barfoo" -> "foobar"))

问题是我不知道如何实现一种通配符函数来匹配 JSON 对象中的所有元素,但不匹配嵌套元素或数组。

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

这是代码草稿:

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

您将无法对此处的所有内容使用 Play JSON 组合器,因为它们仅适用于固定字段映射。为了能够读取 elements 字段,您需要实现 Reads[List[(String, String)]]。幸运的是,Play 已经有可用的 Reads[Map[A, B]](对于也有 ReadsAB 类型),并且 Map[A, B] 可以很容易地转换为List[(A, B)](在Map下面只是元组的集合)。

对于一次性的情况,我们可以使用 read[Map[String, String]]mapList。然后,我们可以将其映射到案例 class。假设以下 JSON 结构:

val js = Json.parse("""{"element": { "foo": "bar", "barfoo": "foobar"}}""")

你可以这样写:

implicit val reads = (__ \ "elements").read[Map[String, String]]
  .map(_.toList)
  .map(tuples => Model(tuples))

并尝试一下:

scala> js.validate[Model]
res8: play.api.libs.json.JsResult[Model] = JsSuccess(Model(List((foo,bar), (barfoo,foobar))),/elements)

请注意,上面的 Reads[Model] 是一种特殊情况,因为 class 只有一个字段。为了进一步了解它如何与 JSON 组合器一起使用,让我们添加一个新字段:

case class Model(elements: List[(String, String)], info: String)

然后,让我们也让元组的 Reads 更通用一些,以便它可以处理任何类型 A 的值,其中 Reads[A] 可用:

implicit def tupleReads[A](implicit rds: Reads[A]): Reads[List[(String, A)]] =
  Reads.mapReads(rds).map(_.toList)

现在我们可以使用组合器为新定义的 Model 编写一个 Reads,就像您习惯的那样:

implicit val reads = (
  (__ \ "elements").read[List[(String, String)]] and
  (__ \ "info").read[String]
)(Model.apply _)

尝试一下:

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

如果你的 JSON 结构 只有 看起来像 {"foo": "bar", "barfoo": "foobar"} (没有 elements 键),那么我们仍然可以利用相同的泛型Reads[List[(String, A)]],但需要实现更自定义的 Reads[Model] 以将整个对象映射到一个模型字段。让我们把上面的 JSON 映射到:

Model(List(("foo" -> "bar"), ("barfoo" -> "foobar")))

我们需要的Reads[Model]和我定义的第一个基本一样,只是我们可以去掉其中的JsPath

// Use `tupleReads` as defined above, restricted to `String`
implicit val reads = tupleReads[String].map(tuples => Model(tuples))

有效:

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