How to write custom circe codec for Map[String, Any]

1.4k Views Asked by At

Is it possible to write custom Decoder for Map[String, Any] using circe? i've found this, but it's only conversion to 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)
  }
2

There are 2 best solutions below

0
On
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))

This is done with generics, I hope it helps

0
On

This Circe decoder will convert its Json into 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

It would be nicer to case match on the cursor value, but those classes are not public. Circe Json class hierarchy

Note that I encoded null as Unit, which isn't quite right -- Scala and null don't play nicely and you may need to tailor your implementation to your use case. I actually omit that case entirely from my own implementation because I'm decoding Json that was encoded from Scala instances.

You also need to create a custom Decoder in io.circe for Java Map and List. I did the following, which uses package-private Circe code, making it concise but also vulnerable to changes in Circe and forcing you to have your own io.circe package.

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]])
}

Fwiw, I'm already thinking of submitting this as a PR to Circe.