17

If I have a Map[String,String]("url" -> "xxx", "title" -> "yyy"), is there an way to generically transform it into a case class Image(url:String, title:String)?

I can write a helper:

object Image{
  def fromMap(params:Map[String,String]) = Image(url=params("url"), title=params("title"))
}

but is there a way to generically write this once for a map to any case class?

tommy chheng
  • 9,108
  • 9
  • 55
  • 72

5 Answers5

8

First off, there are some safe alternatives you could do if you just want to shorten your code. The companion object can be treated as a function so you could use something like this:

def build2[A,B,C](m: Map[A,B], f: (B,B) => C)(k1: A, k2: A): Option[C] = for {
  v1 <- m.get(k1)
  v2 <- m.get(k2)
} yield f(v1, v2)

build2(m, Image)("url", "title")

This will return an option containing the result. Alternatively you could use the ApplicativeBuilders in Scalaz which internally do almost the same but with a nicer syntax:

import scalaz._, Scalaz._
(m.get("url") |@| m.get("title"))(Image)

If you really need to do this via reflection then the easiest way would be to use Paranamer (as the Lift-Framework does). Paranamer can restore the parameter names by inspecting the bytecode so there is a performance hit and it will not work in all environments due to classloader issues (the REPL for example). If you restrict yourself to classes with only String constructor parameters then you could do it like this:

val pn = new CachingParanamer(new BytecodeReadingParanamer)

def fill[T](m: Map[String,String])(implicit mf: ClassManifest[T]) = for {
  ctor <- mf.erasure.getDeclaredConstructors.filter(m => m.getParameterTypes.forall(classOf[String]==)).headOption
  parameters = pn.lookupParameterNames(ctor)
} yield ctor.newInstance(parameters.map(m): _*).asInstanceOf[T]

val img = fill[Image](m)

(Note that this example can pick a default constructor as it does not check for the parameter count which you would want to do)

Moritz
  • 14,144
  • 2
  • 56
  • 55
7

Here's a solution using builtin scala/java reflection:

  def createCaseClass[T](vals : Map[String, Object])(implicit cmf : ClassManifest[T]) = {
      val ctor = cmf.erasure.getConstructors().head
      val args = cmf.erasure.getDeclaredFields().map( f => vals(f.getName) )
      ctor.newInstance(args : _*).asInstanceOf[T]
  }

To use it:

val image = createCaseClass[Image](Map("url" -> "xxx", "title" -> "yyy"))
Andrejs
  • 26,885
  • 12
  • 107
  • 96
  • This is an interesting approach however how to avoid `argument type mismatch` exceptions when instantiating the class? – bachr Dec 08 '16 at 17:20
  • This is the most concise answer to this question that I've seen. However it uses API which is now deprecated. This is easily updated by using "ClassTag" instead of "ClassManifest" and "runtimeClass" instead of "erasure" – Uncle Long Hair Jun 27 '17 at 21:30
2

Not a full answer to your question, but a start…

It can be done, but it will probably get more tricky than you thought. Each generated Scala class is annotated with the Java annotation ScalaSignature, whose bytes member can be parsed to give you the metadata that you would need (including argument names). The format of this signature is not API, however, so you'll need to parse it yourself (and are likely to change the way you parse it with each new major Scala release).

Maybe the best place to start is the lift-json library, which has the ability to create instances of case classes based on JSON data.

Update: I think lift-json actually uses Paranamer to do this, and thus may not parse the bytes of ScalaSignature… Which makes this technique work for non-Scala classes, too.

Update 2: See Moritz's answer instead, who is better informed than I am.

Community
  • 1
  • 1
Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234
  • so why not just create json data and let lift-json do the rest? that way he wouldn't have to update it himself with each new version of scala and wouldn't have to parse the ScalaSignature bytes. Performance would be less than optimal of course, but that might not be an issue for the OP. Am I missing something? – Kim Stebel May 31 '11 at 08:51
  • @Kim Hey, that's a good way to go — if the OP is willing to create JSON just for that purpose. Alternatively, maybe it is possible to reuse lift-json more directly with a `Map[String, String]` as input… – Jean-Philippe Pellet May 31 '11 at 08:55
  • thanks for the tips. Going from Map to JSON string to lift-json parsing to case class seems like a lot of unnecessary serialize/deserialize processing. – tommy chheng Jun 01 '11 at 19:08
0

You can transform map to json and then to case class. It's a bit hacky though.

import spray.json._

object MainClass2 extends App {
  val mapData: Map[Any, Any] =
    Map(
      "one" -> "1",
      "two" -> 2,
      "three" -> 12323232123887L,
      "four" -> 4.4,
      "five" -> false
    )

  implicit object AnyJsonFormat extends JsonFormat[Any] {
    def write(x: Any): JsValue = x match {
      case int: Int           => JsNumber(int)
      case long: Long          => JsNumber(long)
      case double: Double        => JsNumber(double)
      case string: String        => JsString(string)
      case boolean: Boolean if boolean  => JsTrue
      case boolean: Boolean if !boolean => JsFalse
    }
    def read(value: JsValue): Any = value match {
      case JsNumber(int) => int.intValue()
      case JsNumber(long) => long.longValue()
      case JsNumber(double) => double.doubleValue()
      case JsString(string) => string
      case JsTrue      => true
      case JsFalse     => false
    }
  }

  import ObjJsonProtocol._
  val json = mapData.toJson
  val result: TestObj = json.convertTo[TestObj]
  println(result)

}

final case class TestObj(one: String, two: Int, three: Long, four: Double, five: Boolean)

object ObjJsonProtocol extends DefaultJsonProtocol {
  implicit val objFormat: RootJsonFormat[TestObj] = jsonFormat5(TestObj)
}

and use this dependency in sbt build:

 "io.spray"          %%   "spray-json"     %   "1.3.3"
-2

This cannot be done, since you would need to get the companion object's apply method's parameter names and they simply aren't available via reflection. If you have a lot of these case classes, you could parse their declarations and generate the fromMap methods.

Kim Stebel
  • 41,826
  • 12
  • 125
  • 142
  • 2
    They aren't available via standard Java reflection, but you may try your luck parsing the [`ScalaSignature`](http://www.scala-lang.org/sid/10) bytes… – Jean-Philippe Pellet May 31 '11 at 08:01