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

后端 未结 3 2025
挽巷
挽巷 2021-01-17 09:27

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 ca

相关标签:
3条回答
  • 2021-01-17 09:39

    I've found a reasonably neat way to do it using Spray Json

    First we define a way to get to a JsObject from a Map[String, Any]

    def mapToJsObject(map: Map[String, Any]): JsObject =
      JsObject(fields = map.mapValues(anyToJsValue))
    
    def anyToJsValue(any: Any): JsValue = any match {
      case n: Int => JsNumber(n)
      case n: Long => JsNumber(n)
      case n: Double => JsNumber(n)
      case s: String => JsString(s)
      case true => JsTrue
      case false => JsFalse
      case null | None => JsNull
      case list: List[_] => JsArray(list.map(anyToJsValue).toVector)
      case Some(any) => anyToJsValue(any)
      case map: Map[String, Any] => mapToJsObject(map)
    }
    

    Then we can just use convertTo provided we have the implicit JsonFormat in scope

    case class Address(street: String, zip: Int)
    case class Person(name: String, address: Address)
    
    implicit val addressFormat = jsonFormat2(Address.apply)
    implicit val personFormat = jsonFormat2(Person.apply)
    
    "Convert Person example map to Person JsObject" in {
      JsonUtils.mapToJsObject(
        Map(
          "name" -> "Tom",
          "address" -> Map("street" -> "Jefferson st", "zip" -> 10000)
        )
      ).convertTo[Person] must_=== Person("Tom", Address("Jefferson st", 10000))
    }
    

     CAVEATs

    Spray json only has out of box jsonFormat up to 22 fields!

    Can not handle any custom types, e.g. java.sql.Timestamp, since this isn't a JSON type.

    0 讨论(0)
  • 2021-01-17 09:40

    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
    
    0 讨论(0)
  • 2021-01-17 09:43

    Using jackson:

    libraryDependencies += "com.fasterxml.jackson.core" % "jackson-databind" % "2.9.8"
    libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.9.8"
    
    case class Foo(a: List[Int], b: Option[Double])
    case class Bar(c: Int, d: String, e: Foo)
    
    val mapper = new ObjectMapper().registerModule(DefaultScalaModule)
    println(mapper.convertValue(Map(
      "c" -> 3, 
      "d" -> "foo", 
      "e" -> Map("a" -> List(1, 2))), classOf[Bar]))
    

    Output: Bar(3,foo,Foo(List(1, 2),None))

    0 讨论(0)
提交回复
热议问题