简体   繁体   English

如何使用 Scala 中的任何库将通用的潜在嵌套映射 Map[String, Any] 转换为 case 类?

[英]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我对反射并没有很满意,这个答案使用 shapeless 对某些情况有效(但似乎有很多边缘情况) 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?有谁知道一个不错的库可以在几个 LOC 中做到这一点?

I've found a reasonably neat way to do it using Spray Json我找到了一种使用 Spray Json 的相当简洁的方法

First we define a way to get to a JsObject from a Map[String, Any]首先,我们定义了一种从Map[String, Any]获取JsObject的方法

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然后我们可以只使用convertTo前提是我们在范围内有隐式JsonFormat

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! Spray json 只有开箱即用的 jsonFormat 最多 22 个字段!

Can not handle any custom types, eg java.sql.Timestamp , since this isn't a JSON type.无法处理任何自定义类型,例如java.sql.Timestamp ,因为这不是 JSON 类型。

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))输出: Bar(3,foo,Foo(List(1, 2),None))

We can use circe我们可以使用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.然后,如果我们有任何不是原始类型的类型,我们只需要将它们添加到我们的anyToJson以及可以将这种类型编码/解码为原始类型的编码器/解码器对。

Eg we can represent java.sql.Timestamp with Long , then例如,我们可以用Long表示java.sql.Timestamp ,然后

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我们需要anyToJson一行添加到anyToJson

case n: Timestamp => n.asJson

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM