2

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?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Diego Hermida
  • 143
  • 1
  • 7

1 Answers1

7

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))
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Upon running the test cases as above, I get missing implicit error on these two: `default: Default.AsRecord.Aux[A, Defaults` `toMap: ToMap.Aux[Defaults, K, V]` – Jeet Banerjee Mar 24 '23 at 15:42
  • Upon removing the "Default" logic it works fine. How can I add an implicit value for the default? – Jeet Banerjee Mar 24 '23 at 16:15
  • @JeetBanerjee As you can see, the code is working https://scastie.scala-lang.org/DmytroMitin/5zMSectiTcizIaJUvhPZ4w You should better open a new question and describe the whole your setting in details with [MCVE](https://stackoverflow.com/help/minimal-reproducible-example). Not sure what you're asking. – Dmytro Mitin Mar 24 '23 at 17:01