在编译时密钥未知的情况下解码 JSON 值
Decoding JSON values in circe where the key is not known at compile time
假设我一直在与这样的 JSON 合作:
{ "id": 123, "name": "aubergine" }
通过将其解码为 Scala 案例 class,如下所示:
case class Item(id: Long, name: String)
这与 circe 的泛型推导配合得很好:
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))
现在假设我想将本地化信息添加到表示中:
{ "id": 123, "name": { "localized": { "en_US": "eggplant" } } }
我不能通过泛型推导直接使用这样的案例 class:
case class LocalizedString(lang: String, value: String)
…因为语言标签是键,不是字段。我该怎么做,最好不要有太多样板文件?
您可以通过几种不同的方式将单例 JSON 对象解码为类似于 LocalizedString
的案例 class。最简单的是这样的:
import io.circe.Decoder
implicit val decodeLocalizedString: Decoder[LocalizedString] =
Decoder[Map[String, String]].map { kvs =>
LocalizedString(kvs.head._1, kvs.head._2)
}
这样做的缺点是会在空 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")
}
不过,这可能效率低下,尤其是如果您最终可能会尝试解码非常大的 JSON 对象(这将被转换为映射,然后是成对列表,只是为了失败.).
如果你关心性能,你可以这样写:
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)
)
}
}
不过,它解码了单例对象本身,并且在我们想要的表示中,我们有一个 {"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"))
这将适合我们更新的 Item
class:
的通用派生实例
import io.circe.generic.auto._, io.circe.jawn.decode
case class Item(id: Long, name: LocalizedString)
然后:
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))
自定义编码器更直接一点:
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)
)
}
然后:
scala> result.asJson
res11: io.circe.Json =
{
"id" : 123,
"name" : {
"localized" : {
"en_US" : "eggplant"
}
}
}
这种方法适用于任意数量的 "dynamic" 字段,例如,您可以将输入转换为 Map[String, Json]
或 JsonObject
并直接使用键值对.
假设我一直在与这样的 JSON 合作:
{ "id": 123, "name": "aubergine" }
通过将其解码为 Scala 案例 class,如下所示:
case class Item(id: Long, name: String)
这与 circe 的泛型推导配合得很好:
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))
现在假设我想将本地化信息添加到表示中:
{ "id": 123, "name": { "localized": { "en_US": "eggplant" } } }
我不能通过泛型推导直接使用这样的案例 class:
case class LocalizedString(lang: String, value: String)
…因为语言标签是键,不是字段。我该怎么做,最好不要有太多样板文件?
您可以通过几种不同的方式将单例 JSON 对象解码为类似于 LocalizedString
的案例 class。最简单的是这样的:
import io.circe.Decoder
implicit val decodeLocalizedString: Decoder[LocalizedString] =
Decoder[Map[String, String]].map { kvs =>
LocalizedString(kvs.head._1, kvs.head._2)
}
这样做的缺点是会在空 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")
}
不过,这可能效率低下,尤其是如果您最终可能会尝试解码非常大的 JSON 对象(这将被转换为映射,然后是成对列表,只是为了失败.).
如果你关心性能,你可以这样写:
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)
)
}
}
不过,它解码了单例对象本身,并且在我们想要的表示中,我们有一个 {"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"))
这将适合我们更新的 Item
class:
import io.circe.generic.auto._, io.circe.jawn.decode
case class Item(id: Long, name: LocalizedString)
然后:
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))
自定义编码器更直接一点:
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)
)
}
然后:
scala> result.asJson
res11: io.circe.Json =
{
"id" : 123,
"name" : {
"localized" : {
"en_US" : "eggplant"
}
}
}
这种方法适用于任意数量的 "dynamic" 字段,例如,您可以将输入转换为 Map[String, Json]
或 JsonObject
并直接使用键值对.