1

I have a following class

package myapp.model

case class Person(
 name: String,
 age: Option[Int]
)

I would like to implement following function:

def getFieldClass(className: String, fieldName:String): java.lang.Class[_] = {
    // case normal field return its class
    // case Option field return generic type of Option
}

So that for following input:

  • className="myapp.model.Person"
  • fieldName="age"

the function will return class object: scala.Int


Solution with Java Reflection API doesn't work well, it returns java.lang.Object for Option[Int]:

def getFieldClass(className: String, fieldName:String): java.lang.Class[_] = {
    val cls = java.lang.Class.forName(className)
    val pt = cls.getDeclaredField(fieldName).getGenericType.asInstanceOf[java.lang.reflect.ParameterizedType]
    val tpe = pt.getActualTypeArguments()(0);
    java.lang.Class.forName(tpe.getTypeName)
}

I'm writing part of deserializing feature and I don't have the object to check it's type, I have only a class name.

bad_coder
  • 11,289
  • 20
  • 44
  • 72
Tomasz Bekas
  • 666
  • 6
  • 15

2 Answers2

3

You can accomplish this with Scala's reflection library.

It's not especially pretty though:

import scala.reflect.runtime.{ universe => u }
import scala.reflect.runtime.universe._

object ReflectionHelper {

  val classLoader = Thread.currentThread().getContextClassLoader

  val mirror = u.runtimeMirror(classLoader)

  def getFieldType(className: String, fieldName: String): Option[Type] = {

    val classSymbol = mirror.staticClass(className)

    for {
      fieldSymbol <- classSymbol.selfType.members.collectFirst({
        case s: Symbol if s.isPublic && s.name.decodedName.toString() == fieldName => s
      })
    } yield {

      fieldSymbol.info.resultType
    }
  }

  def maybeUnwrapFieldType[A](fieldType: Type)(implicit tag: TypeTag[A]): Option[Type] = {
    if (fieldType.typeConstructor == tag.tpe.typeConstructor) {
      fieldType.typeArgs.headOption
    } else {
      Option(fieldType)
    }
  }

  def getFieldClass(className: String, fieldName: String): java.lang.Class[_] = {

    // case normal field return its class
    // case Option field return generic type of Option

    val result = for {
      fieldType <- getFieldType(className, fieldName)
      unwrappedFieldType <- maybeUnwrapFieldType[Option[_]](fieldType)
    } yield {
      mirror.runtimeClass(unwrappedFieldType)
    }

    // Consider changing return type to: Option[Class[_]]
    result.getOrElse(null)
  }
}

Then:

ReflectionHelper.getFieldClass("myapp.model.Person", "age")  // int
ReflectionHelper.getFieldClass("myapp.model.Person", "name") // class java.lang.String

I would recommend changing the return type of getFieldClass to be optional in case the field value doesn't make sense!

Brian Kent
  • 3,754
  • 1
  • 26
  • 31
  • I have not tested the code, but it does look promising. I'll give you an update in few hours - thanks in advance! – Tomasz Bekas May 04 '16 at 09:02
  • @Brian Kent, is there any reason that when this is used with an object class (with fields) as opposed to a case class, `getFieldClass` returns `Object`? – skylerl Feb 27 '19 at 20:05
  • @skylerl I don't see why it shouldn't work. I did a quick test of `class Foo(val a: Int) { val b: Option[Int] = ??? }` and both worked as expected (in Scala 2.12.8). – Brian Kent Mar 01 '19 at 02:50
  • @BrianKent I only noticed the issue if you do `object MyObject { val a: Option[Int] = None }`, it works fine on classes, it's really weird! – skylerl Mar 01 '19 at 13:42
  • @skylerl I'm surprised it would work at all for an `object` (because of the usage of `mirror.staticClass` instead of `mirror.staticModule`). What version of Scala are you using? – Brian Kent Mar 04 '19 at 15:02
1

Maybe this can help you. TypeTags and manifests.

For example, we can write a method which takes some arbitrary object, and using a TypeTag, prints information about that object’s type arguments:

import scala.reflect.runtime.universe._
def paramInfo[T](x: T)(implicit tag: TypeTag[T]): Unit = {
  val targs = tag.tpe match { case TypeRef(_, _, args) => args }
  println(s"type of $x has type arguments $targs")
}

Here, we write a generic method paramInfo parameterized on T, and we supply an implicit parameter (implicit tag: TypeTag[T]). We can then directly access the type (of type Type) that tag represents using method tpe of TypeTag.

We can then use our method paramInfo as follows:

scala> paramInfo(42)
type of 42 has type arguments List()
scala> paramInfo(List(1, 2))
type of List(1, 2) has type arguments List(Int)
Razvanescu
  • 113
  • 1
  • 1
  • 7
  • As far as I understood scala documentation, TypeTag works at compile time. I have only runtime class name as String only. – Tomasz Bekas May 03 '16 at 20:41
  • Check out this [example](https://stackoverflow.com/questions/1094173/how-do-i-get-around-type-erasure-on-scala-or-why-cant-i-get-the-type-paramete/21640639#21640639). – Razvanescu May 03 '16 at 20:44
  • Well, it doesn't work either for me. I've edited my question and added information that I don't have instance to work with - I have the only the name of the class. If I create instance by reflection with default constructor then fields that are optional are initialized with null value. – Tomasz Bekas May 03 '16 at 21:28