scala-reflect is a compile-time reflection system for Scala (JVM + Scala.js) based on macros and generated metadata descriptors.
It extracts structural type metadata at compile time and exposes it through lightweight descriptors that you can register, query, and use at runtime.
Runtime reflection in Scala and Java has fundamental issues:
- slow and unsafe
- poor generic type resolution
- not available on Scala.js
- difficult to unify across platforms
scala-reflect replaces runtime reflection with:
- compile-time generated metadata
- strongly typed property access
- cross-platform compatibility on JVM and Scala.js
- compile-time
PropertyDescriptorgeneration - safe getter/setter access generation
- support for annotations and generics
- unified model across Scala.js and JVM
- stable type metadata for classes, properties, constructors, and annotations
- a simple runtime registry for descriptors and factories
- a lightweight classloader-style abstraction for descriptor lookup and subtype queries
This project is configured as a cross-project for JVM and Scala.js.
If you publish it under the coordinates from build.sbt, the dependencies are:
// JVM
libraryDependencies += "com.anjunar" %% "scala-reflect" % "1.0.0"
// Scala.js
libraryDependencies += "com.anjunar" %%% "scala-reflect" % "1.0.0"The build is configured for Sonatype Central Portal publishing with sbt 1.11.x.
- a verified namespace in Sonatype Central for the
organizationinbuild.sbt - GPG installed locally
- a GPG key uploaded to a public keyserver
- a Sonatype Central user token
Do not commit Sonatype credentials into this repository.
Create ~/.sbt/1.0/credentials.sbt:
credentials += Credentials(Path.userHome / ".sbt" / "sonatype_central_credentials")Then create ~/.sbt/sonatype_central_credentials:
host=central.sonatype.com
user=<your-sonatype-username>
password=<your-sonatype-token>Alternatively, you can provide the same values via the environment variables
SONATYPE_USERNAME and SONATYPE_PASSWORD.
Create the signed bundle:
sbt publishSignedUpload the bundle to Central Portal:
sbt sonaUploadUpload and trigger release directly:
sbt sonaReleaseOn Windows, if gpg.exe is installed but not yet visible in the current shell
session, you can use:
.\scripts\release-central.ps1The current group ID is com.anjunar. This must match a namespace that is
actually registered and verified in Sonatype Central. If your verified namespace
is instead based on GitHub, for example io.github.anjunar, update
ThisBuild / organization before the first public release.
import reflect.macros.PropertySupport
val properties = PropertySupport.extractPropertiesWithAccessors[User]Instead of:
- discovering structure at runtime
- guessing types
- relying on reflection APIs
You:
- generate a static model at compile time
- use it safely at runtime
- JSON mapping frameworks
- schema generation
- UI binding systems
- validation frameworks
- meta-programming without runtime reflection
Use it if:
- you need structured metadata about types
- you want to avoid runtime reflection
- you build framework-level infrastructure
Avoid it if:
- you only need simple reflection occasionally
- you do not want compile-time macro complexity
- vs Java reflection: faster, safer, deterministic
- vs Scala 3 macros directly: higher-level abstraction
- vs shapeless and derivation libraries: more explicit control model
import reflect.*
import reflect.macros.ReflectMacros
case class Person(name: String, age: Int)
val descriptor = ReflectMacros.reflect[Person]
println(descriptor.typeName) // your.package.Person
println(descriptor.simpleName) // Person
println(descriptor.isCaseClass) // true
println(descriptor.propertyNames.mkString(", "))ClassDescriptor is the central metadata object. It contains:
- the fully-qualified type name
- the simple class name
- annotations on the class
- reflected properties
- constructor metadata
- base types
- type parameters
- flags such as
isAbstract,isFinal, andisCaseClass
Example:
import reflect.*
import reflect.macros.ReflectMacros
case class Person(name: String, age: Int)
val descriptor = ReflectMacros.reflect[Person]
descriptor.typeName // e.g. "example.Person"
descriptor.simpleName // "Person"
descriptor.baseTypes // Array of inherited type names
descriptor.constructors // constructor descriptors
descriptor.properties.map(_.name) // Array("name", "age")ReflectMacros.reflectType[T] returns a TypeDescriptor for arbitrary types, including parameterized ones.
import reflect.*
import reflect.macros.ReflectMacros
case class Box[T](value: T)
val td = ReflectMacros.reflectType[Box[String]]
println(td.typeName) // example.Box[scala.Predef.String]
println(td.isParameterized) // trueimport reflect.macros.ReflectMacros
case class Person(name: String, age: Int)
val descriptor = ReflectMacros.reflect[Person]
val nameProperty =
descriptor.getProperty("name").getOrElse(sys.error("missing property"))
println(nameProperty.propertyType.typeName)
println(nameProperty.isReadable)
println(nameProperty.isWriteable)If you want property accessors in the descriptor, use reflectWithAccessors.
import reflect.*
import reflect.macros.ReflectMacros
case class User(var name: String, age: Int)
val descriptor = ReflectMacros.reflectWithAccessors[User]
val accessor =
descriptor.getPropertyAccessor("name").getOrElse(sys.error("missing accessor"))
val user = User("Ada", 37)
println(accessor.get(user)) // Ada
accessor.set(user, "Grace")
println(user.name) // GraceReflectMacros.makeAccessor builds a typed accessor directly from a selector.
import reflect.macros.ReflectMacros
case class Settings(var theme: String, version: Int)
val themeAccessor = ReflectMacros.makeAccessor[Settings, String](_.theme)
val settings = Settings("light", 1)
println(themeAccessor.get(settings)) // light
if themeAccessor.hasSetter then
themeAccessor.set(settings, "dark")For read-only fields, the generated accessor is read-only:
import reflect.macros.ReflectMacros
case class ReadModel(id: String)
val idAccessor = ReflectMacros.makeAccessor[ReadModel, String](_.id)
println(idAccessor.hasSetter) // falsePropertySupport is useful if you want to expose strongly typed property handles in higher-level APIs such as schemas, filters, forms, or query DSLs.
import reflect.macros.PropertySupport
case class Person(id: String, name: String, age: Int)
val property = PropertySupport.makeProperty[Person, String](_.name)
println(property.name) // name
println(property.descriptor.isReadable)
println(property.get(Person("1", "Ada", 37)))You can also extract all available properties including inherited ones:
import reflect.macros.PropertySupport
trait Audited {
val createdBy: String = "system"
}
case class Document(id: String, title: String) extends Audited
val properties = PropertySupport.extractPropertiesWithAccessors[Document]
println(properties.map(_.name).mkString(", "))ReflectRegistry is the global runtime registry for descriptors.
It can:
- register descriptors
- bind runtime classes
- hold factories for instance creation
- resolve descriptors by type name or simple name
- answer subtype checks
import reflect.*
import reflect.macros.ReflectMacros
case class Query(sql: String)
val descriptor = ReflectMacros.reflect[Query]
ReflectRegistry.clear()
ReflectRegistry.registerByTypeName(
descriptor.typeName,
descriptor
.bindRuntimeClass(classOf[Query])
.bindFactory(() => Query("select * from dual"))
)
val loaded = ReflectRegistry.loadClass(descriptor.typeName)
val instance = ReflectRegistry.createInstance(descriptor.typeName)
println(loaded.map(_.simpleName)) // Some(Query)
println(instance.map(_.asInstanceOf[Query].sql)) // Some(select * from dual)If you want the registry to contain descriptors with property accessors, use register.
import reflect.ReflectRegistry
case class User(var name: String, age: Int)
ReflectRegistry.clear()
ReflectRegistry.register(() => User("Ada", 37))
val accessor =
ReflectRegistry.getPropertyAccessor("your.package.User", "name")
.getOrElse(sys.error("missing accessor"))ReflectClassLoader is a local, classloader-style abstraction on top of descriptors.
Use it when you want scoped registration instead of relying only on the global registry.
import reflect.*
import reflect.macros.ReflectMacros
case class Person(name: String, age: Int)
val loader = ReflectClassLoader.create()
val descriptor = ReflectMacros.reflect[Person]
loader.register[Person](descriptor, () => Person("Ada", 37))
val loaded = loader.loadClass("your.package.Person")
val created = loader.createInstanceAs[Person]("your.package.Person")
println(loaded.map(_.simpleName)) // Some(Person)
println(created.map(_.name)) // Some(Ada)import reflect.*
import reflect.macros.ReflectMacros
case class Person(name: String, age: Int)
val parent = ReflectClassLoader.create()
val child = ReflectClassLoader.createWithParent(parent)
parent.register[Person](ReflectMacros.reflect[Person], () => Person("Ada", 37))
println(child.loadClass("your.package.Person").isDefined) // trueimport reflect.*
import reflect.macros.ReflectMacros
class Animal(val name: String)
class Dog(name: String) extends Animal(name)
val loader = ReflectClassLoader.create()
loader.register[Animal](ReflectMacros.reflect[Animal])
loader.register[Dog](ReflectMacros.reflect[Dog], () => Dog("Milo"))
println(loader.isAssignableFrom("your.package.Dog", "your.package.Animal")) // true
println(loader.getSubTypes("your.package.Animal").map(_.simpleName)) // List(Dog)ReflectClassLoaderWithResources combines descriptor loading with a simple string resource store.
import reflect.*
import reflect.macros.ReflectMacros
case class Person(name: String, age: Int)
val loader = ReflectClassLoaderWithResources()
loader.register[Person](ReflectMacros.reflect[Person], () => Person("Ada", 37))
loader.addResource("person.schema.json", """{"title":"Person"}""")
println(loader.loadClass("your.package.Person").isDefined) // true
println(loader.getResource("person.schema.json")) // Some(...)There are two main ways to create instances:
- Register a factory on a
ClassDescriptorand create instances through the registry or classloader. - Use
ReflectMacros.createInstance[T](...)for case classes.
import reflect.macros.ReflectMacros
case class Person(name: String, age: Int)
val person = ReflectMacros.createInstance[Person]("Ada", 37)
println(person)- This library targets Scala 3.
- Metadata is generated at compile time through macros.
ReflectMacros.createInstanceis intended for case classes.- Descriptors created with
ReflectMacros.reflect[T]do not include property accessors by default. - If you need property accessors inside the descriptor, prefer
ReflectMacros.reflectWithAccessors[T]orReflectRegistry.register. ReflectRegistryis global mutable state. Clear it in tests if isolation matters.
Run the JVM test suite with:
sbt --batch "scala-reflect-jvm/test"Add your license information here.