【问题标题】:Decoding JSON values in circe where the key is not known at compile time在编译时不知道密钥的情况下解码 JSON 值
【发布时间】:2020-01-02 14:47:53
【问题描述】:

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

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

通过将其解码为这样的 Scala 案例类:

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

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

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

...因为语言标签是一个键,而不是一个字段。我该怎么做,最好不要太多样板?

【问题讨论】:

    标签: json scala decoding circe generic-derivation


    【解决方案1】:

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

    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 类的通用派生实例:

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

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

    【讨论】:

    • 请注意,Encoder.AsObject 是 0.12 中的新功能——以前您会使用 ObjectEncoder
    猜你喜欢
    • 1970-01-01
    • 2018-09-05
    • 2021-12-20
    • 2021-11-14
    • 1970-01-01
    • 1970-01-01
    • 2023-04-02
    • 1970-01-01
    • 2013-08-19
    相关资源
    最近更新 更多