在编译时不知道密钥的情况下解码JSON值

Tra*_*own 8 json scala decoding circe generic-derivation

假设我一直在使用这样的JSON:

{ "id": 123, "name": "aubergine" }
Run Code Online (Sandbox Code Playgroud)

通过将其解码为Scala case类,如下所示:

{ "id": 123, "name": "aubergine" }
Run Code Online (Sandbox Code Playgroud)

这与circe的泛型推导工作得很好:

case class Item(id: Long, name: String)
Run Code Online (Sandbox Code Playgroud)

现在假设我要向表示形式添加本地化信息:

{ "id": 123, "name": { "localized": { "en_US": "eggplant" } } }
Run Code Online (Sandbox Code Playgroud)

我不能直接通过泛型派生使用这样的案例类:

scala> import io.circe.generic.auto._, io.circe.jawn.decode
import io.circe.generic.auto._
import io.circe.jawn.decode

scala> decode[Item]("""{ "id": 123, "name": "aubergine" }""")
res1: Either[io.circe.Error,Item] = Right(Item(123,aubergine))
Run Code Online (Sandbox Code Playgroud)

…因为语言标签是键,而不是字段。如何做到这一点,最好不要增加太多样板?

Tra*_*own 9

您可以LocalizedString通过几种不同的方式将单例JSON对象解码为case类。最简单的是这样的:

import io.circe.Decoder

implicit val decodeLocalizedString: Decoder[LocalizedString] =
  Decoder[Map[String, String]].map { kvs =>
    LocalizedString(kvs.head._1, kvs.head._2)
  }
Run Code Online (Sandbox Code Playgroud)

这样做的缺点是,在空的JSON对象上引发异常,并且对于存在多个字段的情况,行为是不确定的。您可以解决以下问题:

implicit val decodeLocalizedString: Decoder[LocalizedString] =
  Decoder[Map[String, String]].map(_.toList).emap {
    case List((k, v)) => Right(LocalizedString(k, v))
    case Nil          => Left("Empty object, expected singleton")
    case _            => Left("Multiply-fielded object, expected singleton")
  }
Run Code Online (Sandbox Code Playgroud)

但是,这可能效率很低,尤其是如果您有机会最终尝试解码非常大的JSON对象(这些对象将转换为map,然后是成对列表,只是失败了)。

如果您担心性能,则可以这样编写:

import io.circe.DecodingFailure

implicit val decodeLocalizedString: Decoder[LocalizedString] = { c =>
  c.value.asObject match {
    case Some(obj) if obj.size == 1 =>
      val (k, v) = obj.toIterable.head
      v.as[String].map(LocalizedString(k, _))
    case None => Left(
      DecodingFailure("LocalizedString; expected singleton object", c.history)
    )
  }
}
Run Code Online (Sandbox Code Playgroud)

不过,这会解码单例对象本身,并且在我们期望的表示形式中,我们有一个{"localized": { ... }}包装器。我们可以在结尾加上一行来容纳它:

implicit val decodeLocalizedString: Decoder[LocalizedString] = 
  Decoder.instance { c =>
    c.value.asObject match {
      case Some(obj) if obj.size == 1 =>
        val (k, v) = obj.toIterable.head
        v.as[String].map(LocalizedString(k, _))
      case None => Left(
        DecodingFailure("LocalizedString; expected singleton object", c.history)
      )
    }
  }.prepare(_.downField("localized"))
Run Code Online (Sandbox Code Playgroud)

这将与我们更新的Item类的通用派生实例完全匹配:

import io.circe.generic.auto._, io.circe.jawn.decode

case class Item(id: Long, name: LocalizedString)
Run Code Online (Sandbox Code Playgroud)

然后:

scala> val doc = """{"id":123,"name":{"localized":{"en_US":"eggplant"}}}"""
doc: String = {"id":123,"name":{"localized":{"en_US":"eggplant"}}}

scala> val Right(result) = decode[Item](doc)
result: Item = Item(123,LocalizedString(en_US,eggplant))
Run Code Online (Sandbox Code Playgroud)

定制的编码器更加简单:

import io.circe.{Encoder, Json, JsonObject}, io.circe.syntax._

implicit val encodeLocalizedString: Encoder.AsObject[LocalizedString] = {
  case LocalizedString(k, v) => JsonObject(
    "localized" := Json.obj(k := v)
  )
}
Run Code Online (Sandbox Code Playgroud)

然后:

scala> result.asJson
res11: io.circe.Json =
{
  "id" : 123,
  "name" : {
    "localized" : {
      "en_US" : "eggplant"
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

这种方法适用于任意数量的“动态”字段,您可以将输入转换为a Map[String, Json]或,JsonObject然后直接使用键值对。