Декодирование значений JSON в Circe, где ключ не известен во время компиляции

Предположим, я работал с некоторым 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)

... потому что языковой тег - это ключ, а не поле. Как я могу это сделать, желательно без лишних шаблонов?

1 ответ

Вы можете декодировать одноэлементный объект JSON в класс case, как 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 и работать с парами ключ-значение напрямую.

Другие вопросы по тегам