A small mapper
Objects to maps and back, built on AnnotationIntrospector. The same shape as json-mapper, without JSON: a practical first step when you write your own framework code.
Model
Opt in with an annotation
The class marks what the mapper may touch. Everything else stays private to it.
java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Exposed {}
scala
class Account {
@(Exposed @field) var owner: String = null
@(Exposed @field) var balance: java.math.BigDecimal = null
var audit: String = "not mapped"
}
Mapper
Two directions, one model
toMap reads every exposed property. fromMap creates an instance through the no-argument constructor and writes every writable property the map contains.
scala
import com.anjunar.scala.universe.TypeResolver
import com.anjunar.scala.universe.introspector.AnnotationIntrospector
object MiniMapper {
def toMap(value: AnyRef): Map[String, Any] =
val model = AnnotationIntrospector.create(TypeResolver.resolve(value.getClass), classOf[Exposed])
model.properties.map(property => property.name -> property.get(value)).toMap
def fromMap[T](clazz: Class[T], values: Map[String, Any]): T =
val resolved = TypeResolver.resolve(clazz)
val instance = resolved.findConstructor().newInstance().asInstanceOf[AnyRef]
AnnotationIntrospector.create(resolved, classOf[Exposed]).properties
.filter(_.isWriteable)
.foreach(property => values.get(property.name).foreach(property.set(instance, _)))
instance.asInstanceOf[T]
}
scala
val account = MiniMapper.fromMap(classOf[Account], Map("owner" -> "Ada", "balance" -> BigDecimal(12).bigDecimal))
println(MiniMapper.toMap(account))
Output
Map(owner -> Ada, balance -> 12)
Next steps
Where a real mapper goes further
The skeleton is the same in json-mapper; the rest is what makes it production code.
What json-mapper adds
Type conversion per propertyType, including collections through typeArguments.
Nested objects, merged in place instead of created anew.
A schema per class, found through companionInstance, with visibility rules.
Validation before every write, with paths for every violation.
Models are cached
AnnotationIntrospector.create keeps one model per class and annotation, so calling it on every request costs a map lookup. Like the type cache, that map is not synchronized; warm it at startup in a server.