Search code examples
scalareflectiongeneric-programmingshapeless

How to convert generic potentially nested map Map[String, Any] to case class using any library in Scala?


I've not had much joy with reflection, this answer using shapeless works for some cases (but seems to have many edge cases) Shapeless code to convert Map[String, Any] to case class cannot handle optional substructures

Does anyone know of a nice library that does this in just a few LOCs?


Solution

  • We can use circe

    import io.circe._
    import io.circe.generic.auto._
    import io.circe.parser._
    import io.circe.syntax._
    
    
    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)
      }
    
    def mapToCaseClass[T : Decoder](map: Map[String, Any]): T = mapToJson(map).as[T].right.get
    

    Then, if we have any types that are not primitive, we just need to add these to our anyToJson along with an encoder/decoder pair that can encode/decode this type as something primitive.

    E.g. we can represent java.sql.Timestamp with Long, then

    import cats.syntax.either._
    
      import io.circe.Decoder
      import io.circe.Encoder
    
      implicit val decodeTimestamp: Decoder[Timestamp] = Decoder.decodeLong.emap(long =>
        Either.catchNonFatal(new Timestamp(long)).leftMap(_ => "Timestamp")
      )
    
    implicit val encodeTimestamp: Encoder[Timestamp] = Encoder.encodeLong.contramap[Timestamp](_.getTime)
    

    and we need to add the line to anyToJson

    case n: Timestamp => n.asJson