Как написать пользовательский кодек circe для карты [Строка, любая]

#scala #circe

#scala #circe

Вопрос:

Можно ли написать пользовательский декодер для Map [String, Any] с помощью circe? я нашел это, но это только преобразование в Json:

 def mapToJson(map: Map[String, Any]): Json =
    map.mapValues(anyToJson).asJson

  def anyToJson(any: Any): Json = any match {
    case n: Int => n.asJson
    case n: Long => n.asJson
    case n: Double => n.asJson
    case s: String => s.asJson
    case true => true.asJson
    case false => false.asJson
    case null | None => None.asJson
    case list: List[_] => list.map(anyToJson).asJson
    case list: Vector[_] => list.map(anyToJson).asJson
    case Some(any) => anyToJson(any)
    case map: Map[String, Any] => mapToJson(map)
  }
  

Комментарии:

1. Вы можете сделать это (вручную), но это почти наверняка неправильно. У вас нет гарантии, что ваш тип среды выполнения будет любым из этих, и для конкретного ADT вы всегда можете получить как декодер, так и кодировщик. Если у вас есть Any где-нибудь, вы, скорее всего, испортили свой дизайн, поскольку вместо этого вы могли бы иметь что-то конкретное и использовать конкретную реализацию для этого типа.

2. Если только вы не вынуждены использовать чью-то ошибочную реализацию, и в этом случае исходная проблема все еще нуждается в решении.

Ответ №1:

Этот декодер Circe преобразует свой Json в Map[String, Any] :

 import io.circe.JavaDecoder._

implicit val objDecoder: Decoder[Any] = {
  case x: HCursor if x.value.isObject =>
    x.value.as[java.util.Map[String, Any]]
  case x: HCursor if x.value.isString =>
    x.value.as[String]
  case x: HCursor if x.value.isBoolean =>
    x.value.as[Boolean]
  case x: HCursor if x.value.isArray =>
    x.value.as[java.util.List[Any]]
  case x: HCursor if x.value.isNumber =>
    x.value.as[Double]
  case x: HCursor if x.value.isNull =>
    x.value.as[Unit]}

import io.circe.parser._

parse("""{"foo": 1, "bar": null, "baz": {"a" : "hi", "b" : true, "dogs" : [{ "name" : "Rover", "age" : 4}, { "name" : "Fido", "age" : 5 }] } }""")
  .getOrElse(Json.Null)
  .as[Map[String, _]]. // <-- this is the call to convert from Circe Json to the Java Map
  

Было бы лучше сопоставить регистр по значению курсора, но эти классы не являются общедоступными.
Иерархия классов Circe Json

Обратите внимание, что я закодировал null как Unit , что не совсем правильно — Scala и null не работают хорошо, и вам может потребоваться адаптировать вашу реализацию к вашему варианту использования. На самом деле я полностью исключаю этот случай из своей собственной реализации, потому что я декодирую Json, который был закодирован из экземпляров Scala.

Вам также необходимо создать пользовательский Decoder io.circe для Java Map и List . Я сделал следующее, в котором используется закрытый для пакета код Circe, что делает его кратким, но также уязвимым для изменений в Circe и заставляет вас иметь свой собственный io.circe пакет.

 package io.circe

import java.util.{List => JavaList, Map => JavaMap}
import scala.collection.mutable
import scala.collection.immutable.{List => ScalaList, Map => ScalaMap}
import scala.jdk.CollectionConverters.{MapHasAsJava, SeqHasAsJava}

object JavaDecoder {
  implicit final def decodeJavaList[A](implicit decodeA: Decoder[A]): Decoder[JavaList[A]] = new SeqDecoder[A, JavaList](decodeA) {
    final protected def createBuilder(): mutable.Builder[A, JavaList[A]] =
      ScalaList.newBuilder[A].mapResult(_.asJava)
  }

  implicit final def decodeJavaMap[K, V](implicit
    decodeK: KeyDecoder[K],
    decodeV: Decoder[V]
  ): Decoder[JavaMap[K, V]] = new JavaMapDecoder[K, V, JavaMap](decodeK, decodeV) {
    final protected def createBuilder(): mutable.Builder[(K, V), JavaMap[K, V]] =
      ScalaMap.newBuilder[K, V].mapResult(_.asJava)
  }
}
  
 package io.circe

import scala.collection.{Map => ScalaMap}
import scala.collection.mutable
import scala.jdk.CollectionConverters.MapHasAsJava

abstract class JavaMapDecoder[K, V, M[K, V] <: java.util.Map[K, V]](
  decodeK: KeyDecoder[K],
  decodeV: Decoder[V]
) extends Decoder[M[K, V]] {

  private val mapDecoder = new MapDecoder[K, V, ScalaMap](decodeK, decodeV) {
    override protected def createBuilder(): mutable.Builder[(K, V), ScalaMap[K, V]] =
      ScalaMap.newBuilder[K, V]
  }

  override def apply(c: io.circe.HCursor): io.circe.Decoder.Result[M[K,V]] =
    mapDecoder.apply(c).map(_.asJava.asInstanceOf[M[K, V]])
}
  

Черт возьми, я уже подумываю о том, чтобы отправить это в качестве PR в Circe.

Ответ №2:

 import io.circe.syntax.EncoderOps
import io.circe.{Encoder, Json}

case class Person(name: String, age: Int)
object Person {
  implicit val decoder: io.circe.Decoder[Person] = io.circe.generic.semiauto.deriveDecoder
  implicit val encoder: io.circe.Encoder[Person] = io.circe.generic.semiauto.deriveEncoder
}

case class Home(area: Int)
object Home {
  implicit val decoder: io.circe.Decoder[Home] = io.circe.generic.semiauto.deriveDecoder
  implicit val encoder: io.circe.Encoder[Home] = io.circe.generic.semiauto.deriveEncoder
}

def jsonPrinter[A](obj: A)(implicit encoder: Encoder[A]): Json =
    obj.asJson

jsonPrinter(Person("Eminem", 30))
jsonPrinter(Home(300))
  

Это делается с помощью дженериков, я надеюсь, это поможет