In Scala, how can I programmatically determine the name of the fields of a case class?
Asked Answered
R

4

9

In Scala, suppose I have a case class like this:

case class Sample(myInt: Int, myString: String)

Is there a way for me to obtain a Seq[(String, Class[_])], or better yet, Seq[(String, Manifest)], describing the case class's parameters?

Randa answered 8/6, 2011 at 17:0 Comment(0)
M
2

It's me again (two years later). Here's a different, different solution using Scala reflection. It is inspired by a blog post, which was itself inspired by a Stack Overflow exchange. The solution below is specialized to the original poster's question above.

In one compilation unit (a REPL :paste or a compiled JAR), include scala-reflect as a dependency and compile the following (tested in Scala 2.11, might work in Scala 2.10):

import scala.language.experimental.macros 
import scala.reflect.macros.blackbox.Context

object CaseClassFieldsExtractor {
  implicit def makeExtractor[T]: CaseClassFieldsExtractor[T] =
    macro makeExtractorImpl[T]

  def makeExtractorImpl[T: c.WeakTypeTag](c: Context):
                              c.Expr[CaseClassFieldsExtractor[T]] = {
    import c.universe._
    val tpe = weakTypeOf[T]

    val fields = tpe.decls.collectFirst {
      case m: MethodSymbol if (m.isPrimaryConstructor) => m
    }.get.paramLists.head

    val extractParams = fields.map { field =>
      val name = field.asTerm.name
      val fieldName = name.decodedName.toString
      val NullaryMethodType(fieldType) = tpe.decl(name).typeSignature

      q"$fieldName -> ${fieldType.toString}"
    }

    c.Expr[CaseClassFieldsExtractor[T]](q"""
      new CaseClassFieldsExtractor[$tpe] {
        def get = Map(..$extractParams)
      }
    """)
  }
}

trait CaseClassFieldsExtractor[T] {
  def get: Map[String, String]
}

def caseClassFields[T : CaseClassFieldsExtractor] =
  implicitly[CaseClassFieldsExtractor[T]].get

And in another compilation unit (the next line in the REPL or code compiled with the previous as a dependency), use it like this:

scala> case class Something(x: Int, y: Double, z: String)
defined class Something

scala> caseClassFields[Something]
res0: Map[String,String] = Map(x -> Int, y -> Double, z -> String)

It seems like overkill, but I haven't been able to get it any shorter. Here's what it does:

  1. The caseClassFields function creates an intermediate CaseClassFieldsExtractor that implicitly comes into existence, reports its findings, and disappears.
  2. The CaseClassFieldsExtractor is a trait with a companion object that defines an anonymous concrete subclass of this trait, using a macro. It is the macro that can inspect your case class's fields because it has rich, compiler-level information about the case class.
  3. The CaseClassFieldsExtractor and its companion object must be declared in a previous compilation unit to the one that examines your case class so that the macro exists at the time you want to use it.
  4. Your case class's type data is passed in through the WeakTypeTag. This evaluates to a Scala structure with lots of pattern matching and no documentation that I could find.
  5. We again assume that there's only one ("primary"?) constructor, but I think all classes defined in Scala can have only one constructor. Since this technique examines the fields of the constructor, not all JVM fields in the class, so it's not susceptible to the lack of generality that marred my previous solution.
  6. It uses quasiquotes to build up an anonymous, concrete subclass of the CaseClassFieldsExtractor.
  7. All that "implicit" business allows the macro to be defined and wrapped up in a function call (caseClassFields) without being called too early, when it's not yet defined.

Any comments that could refine this solution or explain how exactly the "implicits" do what they do (or if they can be removed) are welcome.

Munn answered 10/2, 2016 at 2:0 Comment(2)
This is my favourite answer. Cleaner (if not less complicated) than the others; thanks for coming back to this!Randa
Using this awesome answer in 2018, however I did notice that the order of the fields x,y,z in the case class isn't necessarily preserved in the map object. It works in OP's example of x, y, z but it's clearly visible with longer case classes and other field names.Ketty
R
7

I'm answering my own question to provide a base solution, but I'm looking for alternatives and improvements, too.


One option, also compatible with Java and not restricted to case classes, is to use ParaNamer. In Scala, another option is to parse the ScalaSig bytes attached to generated classfiles. Both solutions won't work in the REPL.

Here's my attempt at extracting the names of the fields from ScalaSig (which uses scalap and Scala 2.8.1):

def valNames[C: ClassManifest]: Seq[(String, Class[_])] = {
  val cls = classManifest[C].erasure
  val ctors = cls.getConstructors

  assert(ctors.size == 1, "Class " + cls.getName + " should have only one constructor")
  val sig = ScalaSigParser.parse(cls).getOrElse(error("No ScalaSig for class " + cls.getName + ", make sure it is a top-level case class"))

  val classSymbol = sig.parseEntry(0).asInstanceOf[ClassSymbol]
  assert(classSymbol.isCase, "Class " + cls.getName + " is not a case class")

  val tableSize = sig.table.size
  val ctorIndex = (1 until tableSize).find { i =>
    sig.parseEntry(i) match {
      case m @ MethodSymbol(SymbolInfo("<init>", owner, _, _, _, _), _) => owner match {
        case sym: SymbolInfoSymbol if sym.index == 0 => true
        case _ => false
      }
      case _ => false
    }
  }.getOrElse(error("Cannot find constructor entry in ScalaSig for class " + cls.getName))

  val paramsListBuilder = List.newBuilder[String]
  for (i <- (ctorIndex + 1) until tableSize) {
    sig.parseEntry(i) match {
      case MethodSymbol(SymbolInfo(name, owner, _, _, _, _), _) => owner match {
        case sym: SymbolInfoSymbol if sym.index == ctorIndex => paramsListBuilder += name
        case _ =>
      }
      case _ =>
    }
  }

  paramsListBuilder.result zip ctors(0).getParameterTypes
}

Disclaimer: I don't really understand the structure of ScalaSig and this should be considered as a heuristics. In particular, this code makes the following assumptions:

  • Case classes have only one constructor.
  • The entry of the signature at position zero is always a ClassSymbol.
  • The relevant constructor of the class is the first MethodEntry with name <init> whose owner has id 0.
  • The parameter names have as owner the constructor entry and always after that entry.

It will fail (because of no ScalaSig) on nested case classes.

This method also only returns Class instances and not Manifests.

Please feel free to suggest improvements!

Randa answered 8/6, 2011 at 17:2 Comment(4)
@ziggystar Would you care to elaborate?Randa
Well, I think that's something I would never attempt to do. Reading stuff out of the class files. I hope it helps you save a whole bunch of work.Rondel
@Rondel That's similar to what lift-json does to provide really cool JSON-to-case-class serialization, I think it's worth discussing.Randa
@Rondel He's not reading out of class files, he's just doing reflection.Jakie
M
3

Here's a different solution that uses plain-Java reflection.

case class Test(unknown1: String, unknown2: Int)
val test = Test("one", 2)

val names = test.getClass.getDeclaredFields.map(_.getName)
// In this example, returns Array(unknown1, unknown2).

To get a Seq[(String, Class[_])], you can do this:

val typeMap = test.getClass.getDeclaredMethods.map({
                x => (x.getName, x.getReturnType)
              }).toMap[String, Class[_]]
val pairs = names.map(x => (x, typeMap(x)))
// In this example, returns Array((unknown1,class java.lang.String), (two,int))

I'm not sure about how to get Manifests.

Munn answered 26/8, 2013 at 5:33 Comment(6)
getDeclaredFields sure works (where would the data be stored if not in fields?) and gives you something more useful and less complicated IMO.Randa
Well, heck--- that's exactly what I was looking for when I started this. (getFields is empty, but somehow I missed getDeclaredFields.) I was referring to the fact that Scala presents class members as functions for which the () is optional, rather than bare fields, and I thought that internally they must have some mangled name. But the getFields method is so easy that I'm going to drastically change my answer.Munn
Given your comment, it seems that you could have come up with this on your own. That's okay--- I searched for and found this question because I needed the answer, too, and I'd much rather get it from the reflection API than digging into class files. But you must have had some reason for rejecting the Java-reflection solution. Why is that? Is there some hidden problem with doing things this way?Munn
I was only interested in the parameters of the class itself, not of all of the fields, and was looking for a way to keep only them. (Those parameters are the ones used in pattern matching, too.)Randa
Got it: with case class Test(x: Int, y: Int) { val z: Int = 3 }, the above method would return x, y, and z, not just x and y. Pattern matching only cares about x and y.Munn
Right, that's what the ScalaSig allowed me to do. Nowadays, I assume there would be a better approach with Scala reflection, though I wouldn't know how.Randa
M
2

It's me again (two years later). Here's a different, different solution using Scala reflection. It is inspired by a blog post, which was itself inspired by a Stack Overflow exchange. The solution below is specialized to the original poster's question above.

In one compilation unit (a REPL :paste or a compiled JAR), include scala-reflect as a dependency and compile the following (tested in Scala 2.11, might work in Scala 2.10):

import scala.language.experimental.macros 
import scala.reflect.macros.blackbox.Context

object CaseClassFieldsExtractor {
  implicit def makeExtractor[T]: CaseClassFieldsExtractor[T] =
    macro makeExtractorImpl[T]

  def makeExtractorImpl[T: c.WeakTypeTag](c: Context):
                              c.Expr[CaseClassFieldsExtractor[T]] = {
    import c.universe._
    val tpe = weakTypeOf[T]

    val fields = tpe.decls.collectFirst {
      case m: MethodSymbol if (m.isPrimaryConstructor) => m
    }.get.paramLists.head

    val extractParams = fields.map { field =>
      val name = field.asTerm.name
      val fieldName = name.decodedName.toString
      val NullaryMethodType(fieldType) = tpe.decl(name).typeSignature

      q"$fieldName -> ${fieldType.toString}"
    }

    c.Expr[CaseClassFieldsExtractor[T]](q"""
      new CaseClassFieldsExtractor[$tpe] {
        def get = Map(..$extractParams)
      }
    """)
  }
}

trait CaseClassFieldsExtractor[T] {
  def get: Map[String, String]
}

def caseClassFields[T : CaseClassFieldsExtractor] =
  implicitly[CaseClassFieldsExtractor[T]].get

And in another compilation unit (the next line in the REPL or code compiled with the previous as a dependency), use it like this:

scala> case class Something(x: Int, y: Double, z: String)
defined class Something

scala> caseClassFields[Something]
res0: Map[String,String] = Map(x -> Int, y -> Double, z -> String)

It seems like overkill, but I haven't been able to get it any shorter. Here's what it does:

  1. The caseClassFields function creates an intermediate CaseClassFieldsExtractor that implicitly comes into existence, reports its findings, and disappears.
  2. The CaseClassFieldsExtractor is a trait with a companion object that defines an anonymous concrete subclass of this trait, using a macro. It is the macro that can inspect your case class's fields because it has rich, compiler-level information about the case class.
  3. The CaseClassFieldsExtractor and its companion object must be declared in a previous compilation unit to the one that examines your case class so that the macro exists at the time you want to use it.
  4. Your case class's type data is passed in through the WeakTypeTag. This evaluates to a Scala structure with lots of pattern matching and no documentation that I could find.
  5. We again assume that there's only one ("primary"?) constructor, but I think all classes defined in Scala can have only one constructor. Since this technique examines the fields of the constructor, not all JVM fields in the class, so it's not susceptible to the lack of generality that marred my previous solution.
  6. It uses quasiquotes to build up an anonymous, concrete subclass of the CaseClassFieldsExtractor.
  7. All that "implicit" business allows the macro to be defined and wrapped up in a function call (caseClassFields) without being called too early, when it's not yet defined.

Any comments that could refine this solution or explain how exactly the "implicits" do what they do (or if they can be removed) are welcome.

Munn answered 10/2, 2016 at 2:0 Comment(2)
This is my favourite answer. Cleaner (if not less complicated) than the others; thanks for coming back to this!Randa
Using this awesome answer in 2018, however I did notice that the order of the fields x,y,z in the case class isn't necessarily preserved in the map object. It works in OP's example of x, y, z but it's clearly visible with longer case classes and other field names.Ketty
T
0

addition to Jim Pivarski answer, to have right order of fields, need to add sorted after decls

val fields = tpe.decls.sorted.collectFirst {
  case m: MethodSymbol if (m.isPrimaryConstructor) => m
}.get.paramLists.head
Turpin answered 1/8, 2023 at 15:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.