私が次のようないくつかの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オブジェクトを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": { ... }}
ラッパー。最後に1行追加するだけで対応できます。
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
を使用して、キーと値のペアを直接操作します。