78

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.

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
Will
  • 1,711
  • 1
  • 12
  • 17
  • I found [this blog post](http://blog.echo.sh/post/65955606729/exploring-scala-macros-map-to-case-class-conversion) that shows how to use macros to do this. – Giovanni Botta Dec 17 '13 at 19:42

12 Answers12

96

This should work:

def getCCParams(cc: AnyRef) =
  cc.getClass.getDeclaredFields.foldLeft(Map.empty[String, Any]) { (a, f) =>
    f.setAccessible(true)
    a + (f.getName -> f.get(cc))
  }
Joan
  • 4,079
  • 2
  • 28
  • 37
Walter Chang
  • 11,547
  • 2
  • 47
  • 36
  • 16
    If this is not difficult can you explain what you wrote? – den bardadym Jul 02 '11 at 17:46
  • Now there is scala reflection! I'm not sure if it is still experimental or if it is stable, now. Anyway maybe scala reflection API delivers an own solution or at least a more scala way to implement a solution like the one above. And by the way: when you use setAccessible to true you can access private fields, too. Is this really what you want? And it may not work when a SecurityManager is active. – user573215 Aug 30 '13 at 12:58
  • 2
    @RobinGreen case classes can't inherit from each other – Giovanni Botta Dec 17 '13 at 19:16
  • To make sure it's actually an accessible field, I would use: `cc.getClass.getDeclaredFields.filter { f => val accessor = cc.getClass.getMethod(f.getName); accessor != null && accessor.isAccessible }` – Giovanni Botta Dec 17 '13 at 19:17
  • Actually scratch that comment, I thought it worked but it doesn't. I am not clear why though. – Giovanni Botta Dec 17 '13 at 19:27
  • 1
    @GiovanniBotta The problem seems to be that the accessor method isn't marked as accessible. Weird, as it's a public method. In any case, it should be fine to take out the `isAccessible`, as `getMethod` only returns public methods. Also, `accessor != null` is the wrong test, as `getMethod` throws a `NoSuchMethodException` if the method isn't found. – James_pic Feb 14 '14 at 11:43
  • 2
    probably in this way is easier to understand: `case class Person(name: String, surname: String) val person = new Person("Daniele", "DalleMule") val personAsMap = person.getClass.getDeclaredFields.foldLeft(Map[String, Any]())((map, field) => { field.setAccessible(true) map + (field.getName -> field.get(person)) } )` – DanieleDM Jun 30 '15 at 08:08
  • Thanks. What I added was skipping over None values. val v = f.get(this) val aa = v match { case None => a case _ => a + (f.getName -> v) } aa – Shushu Jul 12 '16 at 09:44
  • I'd also add `.filterNot(_.isSynthetic)` to remove autogenerated fields (e.g. `$outer`), especially, if you want to add on the trait level, that your case classes inherit from. – pcejrowski Sep 04 '18 at 09:08
48

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.

Andrejs
  • 26,885
  • 12
  • 107
  • 96
  • 7
    The `getDeclaredFields` specification says: "The elements in the array returned are not sorted and are not in any particular order." How come the fields return in the correct order? – Giovanni Botta Dec 17 '13 at 19:29
  • True, it's best to check on your jvm/os but in practice http://stackoverflow.com/a/5004929/1180621 – Andrejs Dec 18 '13 at 13:21
  • 3
    Yeah I wouldn't take that for granted though. I don't want to start writing non portable code. – Giovanni Botta Dec 18 '13 at 14:41
  • This will throw an exception if the case class is nested in another object because the productIterator will not contain the "$outer" declared field. – ssice Apr 05 '17 at 15:37
38

Starting Scala 2.13, case classes (as implementations of Product) are provided with a productElementNames method which returns an iterator over their field's names.

By zipping field names with field values obtained with productIterator we can generically obtain the associated Map:

// case class MyClass(param1: String, param2: String)
// val x = MyClass("hello", "world")
(x.productElementNames zip x.productIterator).toMap
// Map[String,Any] = Map("param1" -> "hello", "param2" -> "world")
Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
12

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.

7

Here's a simple variation if you don't care about making it a generic function:

case class Person(name:String, age:Int)

def personToMap(person: Person): Map[String, Any] = {
  val fieldNames = person.getClass.getDeclaredFields.map(_.getName)
  val vals = Person.unapply(person).get.productIterator.toSeq
  fieldNames.zip(vals).toMap
}

scala> println(personToMap(Person("Tom", 50)))
res02: scala.collection.immutable.Map[String,Any] = Map(name -> Tom, age -> 50)
ShawnFumo
  • 2,108
  • 1
  • 25
  • 14
6

If you happen to be using Json4s, you could do the following:

import org.json4s.{Extraction, _}

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

Extraction.decompose(x)(DefaultFormats).values.asInstanceOf[Map[String,String]]
Barak BN
  • 442
  • 6
  • 14
4

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

Community
  • 1
  • 1
harrylaou
  • 59
  • 1
  • 2
4

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
}
Stefan Endrullis
  • 4,150
  • 2
  • 32
  • 45
2

I don't know about nice... but this seems to work, at least for this very very basic example. It probably needs some work but might be enough to get you started? Basically it filters out all "known" methods from a case class (or any other class :/ )

object CaseMappingTest {
  case class MyCase(a: String, b: Int)

  def caseClassToMap(obj: AnyRef) = {
    val c = obj.getClass
    val predefined = List("$tag", "productArity", "productPrefix", "hashCode",
                          "toString")
    val casemethods = c.getMethods.toList.filter{
      n =>
        (n.getParameterTypes.size == 0) &&
        (n.getDeclaringClass == c) &&
        (! predefined.exists(_ == n.getName))

    }
    val values = casemethods.map(_.invoke(obj, null))
    casemethods.map(_.getName).zip(values).foldLeft(Map[String, Any]())(_+_)
  }

  def main(args: Array[String]) {
    println(caseClassToMap(MyCase("foo", 1)))
    // prints: Map(a -> foo, b -> 1)
  }
}
André Laszlo
  • 15,169
  • 3
  • 63
  • 81
2
commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)

Details: https://github.com/hank-whu/common4s

Kai Han
  • 59
  • 1
  • 5
2

With the use of Java reflection, but no change of access level. Converts Product and case class to Map[String, String]:

def productToMap[T <: Product](obj: T, prefix: String): Map[String, String] = {
  val clazz = obj.getClass
  val fields = clazz.getDeclaredFields.map(_.getName).toSet
  val methods = clazz.getDeclaredMethods.filter(method => fields.contains(method.getName))
  methods.foldLeft(Map[String, String]()) { case (acc, method) =>
    val value = method.invoke(obj).toString
    val key = if (prefix.isEmpty) method.getName else s"${prefix}_${method.getName}"
    acc + (key -> value)
  }
}
Artavazd Balayan
  • 2,353
  • 1
  • 16
  • 25
0

Modern variation with Scala 3 might also be a bit simplified as with the following example that is similar to the answer posted by Walter Chang above.

def getCCParams(cc: AnyRef): Map[String, Any] =
  cc.getClass.getDeclaredFields
    .tapEach(_.setAccessible(true))
    .foldLeft(Map.empty)((a, f) => a + (f.getName -> f.get(cc)))
Oto Brglez
  • 4,113
  • 1
  • 26
  • 33