Search code examples
scalareflectiondefault-valuedefault-parametersscala-reflect

Instantiating Scala class/case class via reflection


Description

I'm trying to build a tool capable of converting a Map[String, Any] into a class/case class instance. If the class definition contains default parameters which are not specified in the Map, then the default values would apply.

The following code snippet allows retrieving the primary class constructor:

import scala.reflect.runtime.universe._

def constructorOf[A: TypeTag]: MethodMirror = {
  val constructor = typeOf[A]
    .decl(termNames.CONSTRUCTOR)
    // Getting all the constructors
    .alternatives
    .map(_.asMethod)
    // Picking the primary constructor
    .find(_.isPrimaryConstructor)
    // A class must always have a primary constructor, so this is safe
    .get
  typeTag[A].mirror
    .reflectClass(typeOf[A].typeSymbol.asClass)
    .reflectConstructor(constructor)
}

Given the following simple class definition:

class Foo(a: String = "foo") {
  override def toString: String = s"Foo($a)"
}

I can easily create a new Foo instance when providing both arguments:

val bar = constructorOf[Foo].apply("bar").asInstanceOf[Foo]
bar: Foo = Foo(bar)

The problem arises when attempting to create an instance without specifying the constructor parameter (which should still work, as parameter a has a default value):

val foo = constructorOf[Foo].apply()
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
    at scala.collection.mutable.WrappedArray$ofRef.apply(WrappedArray.scala:127)
    at scala.reflect.runtime.JavaMirrors$JavaMirror$JavaVanillaMethodMirror2.jinvokeraw(JavaMirrors.scala:384)
    at scala.reflect.runtime.JavaMirrors$JavaMirror$JavaMethodMirror.jinvoke(JavaMirrors.scala:339)
    at scala.reflect.runtime.JavaMirrors$JavaMirror$JavaVanillaMethodMirror.apply(JavaMirrors.scala:355)

Already tried

I've already seen similar questions like this, and tried this one, which didn't work for me:

Exception in thread "main" java.lang.NoSuchMethodException: Foo.$lessinit$greater$default$1()
    at java.lang.Class.getMethod(Class.java:1786)
    at com.dhermida.scala.jdbc.Test$.extractDefaultConstructorParamValue(Test.scala:16)
    at com.dhermida.scala.jdbc.Test$.main(Test.scala:10)
    at com.dhermida.scala.jdbc.Test.main(Test.scala)

Goal

I would like to invoke a class' primary constructor, using the default constructor values in case these are not set in the input Map. My initial approach consists of:

  1. [Done] Retrieve the class' primary constructor.
  2. [Done] Identify which constructor argument(s) have a default parameter.
  3. Call constructorOf[A].apply(args: Any*) constructor, using the default values for any argument not present in the input MapNot working.

Is there any way to retrieve the primary constructor's default argument values using Scala Reflection API?


Solution

  • Accessing default values of method parameters via reflection at runtime is a little tricky: How do I access default parameter values via Scala reflection?

    Are you sure you have to transform Map[String, Any] into a case class at runtime? Wouldn't it be enough for you to do this at compile time?

    For example with Shapeless

    import shapeless.ops.maps.FromMap
    import shapeless.ops.record.ToMap
    import shapeless.{Default, HList, LabelledGeneric}
    
    def fromMapToCaseClassWithDefaults[A <: Product] = new PartiallyApplied[A]
    
    class PartiallyApplied[A <: Product] {
      def apply[Defaults <: HList, K <: Symbol, V, ARecord <: HList](
        m: Map[String, Any]
      )(implicit
        default: Default.AsRecord.Aux[A, Defaults],
        toMap: ToMap.Aux[Defaults, K, V],
        gen: LabelledGeneric.Aux[A, ARecord],
        fromMap: FromMap[ARecord]
      ): Option[A] = {
        import shapeless.record._
        val defaults: Map[Symbol, Any] = default().toMap[K, V].map { case (k, v) => k -> v }
        import shapeless.syntax.std.maps._
        val mWithSymbolKeys: Map[Symbol, Any] = m.map { case (k, v) => Symbol(k) -> v }
        (defaults ++ mWithSymbolKeys).toRecord[ARecord].map(LabelledGeneric[A].from)
      }
    }
    
    case class Foo(a: String = "foo")
    
    fromMapToCaseClassWithDefaults[Foo](Map("a" -> "bar")) //Some(Foo(bar))
    fromMapToCaseClassWithDefaults[Foo](Map()) //Some(Foo(foo))