简体   繁体   English

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

[英]Decoding JSON values in circe where the key is not known at compile time

Suppose I've been working with some JSON like this: 假设我一直在使用这样的JSON:

{ "id": 123, "name": "aubergine" }

By decoding it into a Scala case class like this: 通过将其解码为Scala案例类,如下所示:

case class Item(id: Long, name: String)

This works just fine with circe's generic derivation: 这与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))

Now suppose I want to add localization information to the representation: 现在假设我想在表示中添加本地化信息:

{ "id": 123, "name": { "localized": { "en_US": "eggplant" } } }

I can't use a case class like this directly via generic derivation: 我不能通过泛型派生直接使用这样的case类:

case class LocalizedString(lang: String, value: String)

…because the language tag is a key, not a field. ...因为语言标签是键,而不是字段。 How can I do this, preferably without too much boilerplate? 我怎么能这样做,最好没有太多的样板?

You can decode a singleton JSON object into a case class like LocalizedString in a few different ways. 您可以通过几种不同的方式将单例JSON对象解码为类似LocalizedString的案例类。 The easiest would be something like this: 最简单的是这样的:

import io.circe.Decoder

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

This has the disadvantage of throwing an exception on an empty JSON object, and in the behavior being undefined for cases where there's more than one field. 这样做的缺点是在空JSON对象上抛出异常,并且对于存在多个字段的情况,行为未定义。 You could fix those issues like this: 你可以修复这样的问题:

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

This is potentially inefficient, though, especially if there's a chance you might end up trying to decode really big JSON objects (which would be converted into a map, then a list of pairs, just to fail.). 然而,这可能是低效的,特别是如果你有可能最终尝试解码真正大的JSON对象(它们将被转换为映射,然后是对的列表,只是失败。)。

If you're concerned about performance, you could write something like this: 如果你担心性能,你可以这样写:

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

That decodes the singleton object itself, though, and in our desired representation we have a {"localized": { ... }} wrapper. 但是,这解码了单例对象本身,并且在我们想要的表示中,我们有一个{"localized": { ... }}包装器。 We can accommodate that with a single extra line at the end: 我们可以在最后添加一条额外的线:

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

This will fit right in with a generically derived instance for our updated Item class: 这适用于我们更新的Item类的一般派生实例:

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

case class Item(id: Long, name: LocalizedString)

And then: 然后:

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

The customized encoder is a little more straightforward: 定制编码器更直接:

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

And then: 然后:

scala> result.asJson
res11: io.circe.Json =
{
  "id" : 123,
  "name" : {
    "localized" : {
      "en_US" : "eggplant"
    }
  }
}

This approach will work for any number of "dynamic" fields like this—you can transform the input into either a Map[String, Json] or JsonObject and work with the key-value pairs directly. 这种方法适用于任何数量的“动态”字段 - 您可以将输入转换为Map[String, Json]JsonObject并直接使用键值对。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM