Case class to map in Scala

2019-01-08 04:14发布

Is there a nice way I can convert a Scala case class instance, e.g.

case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")

into a mapping of some kind, e.g.

getCCParams(x) returns "param1" -> "hello", "param2" -> "world"

Which works for any case class, not just predefined ones. I've found you can pull the case class name out by writing a method that interrogates the underlying Product class, e.g.

def getCCName(caseobj: Product) = caseobj.productPrefix 
getCCName(x) returns "MyClass"

So I'm looking for a similar solution but for the case class fields. I'd imagine a solution might have to use Java reflection, but I'd hate to write something that might break in a future release of Scala if the underlying implementation of case classes changes.

Currently I'm working on a Scala server and defining the protocol and all its messages and exceptions using case classes, as they are such a beautiful, concise construct for this. But I then need to translate them into a Java map to send over the messaging layer for any client implementation to use. My current implementation just defines a translation for each case class separately, but it would be nice to find a generalised solution.

10条回答
Fickle 薄情
2楼-- · 2019-01-08 04:31

If anybody looks for a recursive version, here is the modification of @Andrejs's solution:

def getCCParams(cc: Product): Map[String, Any] = {
  val values = cc.productIterator
  cc.getClass.getDeclaredFields.map {
    _.getName -> (values.next() match {
      case p: Product if p.productArity > 0 => getCCParams(p)
      case x => x
    })
  }.toMap
}

It also expands the nested case-classes into maps at any level of nesting.

查看更多
唯我独甜
3楼-- · 2019-01-08 04:33

Because case classes extend Product one can simply use .productIterator to get field values:

def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
                .zip( cc.productIterator.to ).toMap // zipped with all values

Or alternatively:

def getCCParams(cc: Product) = {          
      val values = cc.productIterator
      cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}

One advantage of Product is that you don't need to call setAccessible on the field to read its value. Another is that productIterator doesn't use reflection.

Note that this example works with simple case classes that don't extend other classes and don't declare fields outside the constructor.

查看更多
来,给爷笑一个
4楼-- · 2019-01-08 04:38

Solution with ProductCompletion from interpreter package:

import tools.nsc.interpreter.ProductCompletion

def getCCParams(cc: Product) = {
  val pc = new ProductCompletion(cc)
  pc.caseNames.zip(pc.caseFields).toMap
}
查看更多
手持菜刀,她持情操
5楼-- · 2019-01-08 04:39

You could use shapeless.

Let

case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)

Define a LabelledGeneric representation

import shapeless._
import shapeless.ops.product._
import shapeless.syntax.std.product._
object X {
  implicit val lgenX = LabelledGeneric[X]
}
object Y {
  implicit val lgenY = LabelledGeneric[Y]
}

Define two typeclasses to provide the toMap methods

object ToMapImplicits {

  implicit class ToMapOps[A <: Product](val a: A)
    extends AnyVal {
    def mkMapAny(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, Any] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v }
  }

  implicit class ToMapOps2[A <: Product](val a: A)
    extends AnyVal {
    def mkMapString(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, String] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v.toString }
  }
}

Then you can use it like this.

object Run  extends App {
  import ToMapImplicits._
  val x: X = X(true, "bike",26)
  val y: Y = Y("first", "second")
  val anyMapX: Map[String, Any] = x.mkMapAny
  val anyMapY: Map[String, Any] = y.mkMapAny
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)

  val stringMapX: Map[String, String] = x.mkMapString
  val stringMapY: Map[String, String] = y.mkMapString
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)
}

which prints

anyMapX = Map(c -> 26, b -> bike, a -> true)

anyMapY = Map(b -> second, a -> first)

stringMapX = Map(c -> 26, b -> bike, a -> true)

stringMapY = Map(b -> second, a -> first)

For nested case classes, (thus nested maps) check another answer

查看更多
登录 后发表回答