scala-universe is a JVM-based type introspection library providing a structured and resolved view of classes, generics, and annotations.
It wraps low-level reflection APIs in a higher-level model built around:
- fully resolved types
- unified access to fields, methods, constructors, and parameters
- proper generic type resolution
- bean-style property introspection
- annotation-driven property introspection
- classpath scanning with an annotation index
Java reflection is often:
- low-level
- inconsistent with generics
- difficult to compose
- hard to reason about
scala-universe provides a cleaner abstraction layer for runtime introspection on the JVM.
- Resolve a
java.lang.reflect.Typeinto a reusableResolvedClass - Inspect class hierarchies with generic type resolution
- Find fields, methods, constructors, and parameters in a consistent way
- Access bean properties through getter/setter conventions
- Build property models from annotations
- Scan a package and index discovered annotations
Add the dependency to your build.sbt:
libraryDependencies += "com.anjunar" %% "scala-universe" % "1.0.0"import com.anjunar.scala.universe.TypeResolver
class User(val id: Long, val name: String)
val clazz = TypeResolver.resolve(classOf[User])import com.anjunar.scala.universe.TypeResolver
val resolved = TypeResolver.resolve(classOf[String])
println(resolved.name) // String
println(resolved.fullName) // java.lang.String
println(resolved.raw) // class java.lang.Stringimport com.anjunar.scala.universe.TypeResolver
class User(private var id: Long, val name: String) {
def greeting(prefix: String): String = s"$prefix $name"
}
val userClass = TypeResolver.resolve(classOf[User])
val field = userClass.findField("name")
println(field.name) // name
println(field.fieldType.fullName) // java.lang.String
val method = userClass.findMethod("greeting", classOf[String])
println(method.name) // greeting
println(method.returnType.fullName) // java.lang.String
println(method.parameters.head.name) // prefiximport com.anjunar.scala.universe.TypeResolver
class User(val id: Long, val name: String)
val resolved = TypeResolver.resolve(classOf[User])
val constructor = resolved.findDeclaredConstructor(classOf[Long], classOf[String])
val user = constructor.newInstance(Long.box(1L), "Ada").asInstanceOf[User]
println(user.name) // AdaOne of the main benefits of the library is that inherited generic members are resolved against the concrete subtype.
import com.anjunar.scala.universe.TypeResolver
abstract class Box[T] {
def getValue: T
}
class StringBox extends Box[String] {
override def getValue: String = "hello"
}
val resolved = TypeResolver.resolve(classOf[StringBox])
val method = resolved.findMethod("getValue")
println(method.returnType.raw) // class java.lang.StringInstead of working directly with:
- raw
Class TypeParameterizedType
You work with a clean, unified abstraction layer centered around ResolvedClass and the related member models.
BeanIntrospector creates a BeanModel based on JavaBean-style getters and setters.
import com.anjunar.scala.universe.introspector.BeanIntrospector
class Person {
private var firstName: String = "Ada"
def getFirstName: String = firstName
def setFirstName(value: String): Unit = firstName = value
}
val beanModel = BeanIntrospector.createWithType(classOf[Person])
val property = beanModel.findProperty("firstName")
val person = new Person
println(property.propertyType.fullName) // java.lang.String
println(property.get(person)) // Ada
property.set(person, "Grace")
println(property.get(person)) // GraceAnnotationIntrospector builds a property model from annotated fields and methods.
Define a runtime annotation:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Exposed {
}Then use it in your model:
import com.anjunar.scala.universe.introspector.AnnotationIntrospector
class Account {
@Exposed
private var owner: String = "Ada"
@Exposed
def getOwner: String = owner
}
val model = AnnotationIntrospector.createWithType(classOf[Account], classOf[Exposed])
val property = model.findProperty("owner")
println(property.name) // owner
println(property.propertyType.fullName) // java.lang.StringYou can resolve the generated companion class and instance for Scala types.
import com.anjunar.scala.universe.TypeResolver
class Service
object Service
val companionClass = TypeResolver.companionClass(classOf[Service])
val companion = TypeResolver.companionInstance[Service.type](classOf[Service])
println(companionClass.getName) // Service$
println(companion eq Service) // trueClassPathResolver can scan a package prefix and build an annotation index.
import com.anjunar.scala.universe.ClassPathResolver
val classes =
ClassPathResolver.process(
packagePrefix = "com.example",
classLoader = Thread.currentThread().getContextClassLoader
)
println(classes.size)
val services = ClassPathResolver.findAnnotation(classOf[jakarta.inject.Singleton])
println(services.size)TypeResolver: entry point for resolvingTypevalues and Scala companionsResolvedClass: resolved view over a class or generic typeResolvedField,ResolvedMethod,ResolvedConstructor,ResolvedParameter: member abstractionsBeanIntrospector/BeanModel: bean-style property modelAnnotationIntrospector/AnnotationModel: annotation-driven property modelClassPathResolver: package scanning and annotation indexing
Use scala-universe if:
- you build frameworks or infrastructure
- you need reliable generic type handling
- you want a structured reflection model
- vs Java Reflection: higher-level and easier to reason about
- vs
scala-reflect: runtime-focused instead of compile-time-focused - vs frameworks such as Spring: explicit model without hidden magic
Run the test suite with:
sbt testThis project is configured for the Sonatype Central Portal, not the legacy OSSRH flow.
Required project settings live in build.sbt:
- POM metadata such as
homepage,description,licenses,scmInfo, anddevelopers versionScheme := Some("early-semver")pomIncludeRepository := { _ => false }publishMavenStyle := truepublishTo := if (isSnapshot.value) Some("central-snapshots" at "https://central.sonatype.com/repository/maven-snapshots/") else localStaging.value
The project also uses project/plugins.sbt with:
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")Credentials are intentionally kept outside the repository.
Create ~/.sbt/1.0/credentials.sbt with:
credentials += Credentials(Path.userHome / ".sbt" / "sonatype_central_credentials")Create ~/.sbt/sonatype_central_credentials with:
host=central.sonatype.com
user=<sonatype-user>
password=<sonatype-token>From sbt:
sbt publishSigned
sbt sonaUpload
sbt sonaReleaseOn Windows, use scripts/release-central.ps1 so gpg.exe is available in the current process PATH:
.\scripts\release-central.ps1 publishSigned
.\scripts\release-central.ps1 sonaUpload
.\scripts\release-central.ps1 sonaReleasepublishSignedstages and signs artifacts locally.sonaUploaduploads the bundle to the Sonatype Central Portal.sonaReleaseuploads and publishes in one step.- A newly uploaded public key may take some time to propagate before Sonatype accepts signatures.
- Use
publish / skip := trueonly for a non-published aggregator root in a multi-module build. This repository is currently single-module, so the root project remains publishable.