簡體   English   中英

在編譯時未知密鑰的情況下解碼JSON值

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

假設我一直在使用這樣的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類:

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

...因為語言標簽是鍵,而不是字段。 我怎么能這樣做,最好沒有太多的樣板?

您可以通過幾種不同的方式將單例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並直接使用鍵值對。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM