Fork me on GitHub

Chimney


Scala library for boilerplate-free data transformations

What Chimney does


In the daily life of a strongly-typed language's programmer sometimes it happens we need to transform an object of one type to another object which contains a number of the same or similar fields in their definitions.

case class MakeCoffee(id: Int, kind: String, addict: String)
case class CoffeeMade(id: Int, kind: String, forAddict: String, at: ZonedDateTime)

Usual approach is to just rewrite fields one by one

val command = MakeCoffee(id = Random.nextInt,
                         kind = "Espresso",
                         addict = "Piotr")
val event = CoffeeMade(id = command.id,
                       kind = command.kind,
                       forAddict = command.addict,
                       at = ZonedDateTime.now)

While the example stays lean, in real-life code we usually end up with tons of such boilerplate, especially when:

Chimney provides a compact DSL with which you can define transformation rules and transform your objects with as little boilerplate as possible.

import io.scalaland.chimney.dsl._

val event = command.into[CoffeeMade]
  .withFieldComputed(_.at, _ => ZonedDateTime.now)
  .withFieldRenamed(_.addict, _.forAddict)
  .transform

Underneath it uses Scala macros to give you:

Getting started


To include Chimney to your SBT project, add the following line to your build.sbt:

libraryDependencies += "io.scalaland" %% "chimney" % "0.3.0"

Library is released for Scala 2.11.x, 2.12.x and 2.13.0-M5. If you want to use it with Scala.js(or Scala Native), you need to replace %% with %%%. Due to some compiler bugs, it's recommended to use at least Scala 2.11.9 or 2.12.1.

Trying with Ammonite REPL

The quickest way to try out Chimney is to use a script that downloads coursier and uses it to fetch Ammonite REPL with the latest version of Chimney. It drops you immediately into a REPL session.
curl -s https://raw.githubusercontent.com/scalalandio/chimney/master/try-chimney.sh | bash
Loading...
Welcome to the Ammonite Repl 1.1.0
(Scala 2.12.4 Java 1.8.0_152)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ case class Foo(x: String, y: Int)
defined class Foo

@ case class Bar(x: String, y: Int, z: Boolean = true)
defined class Bar

@ Foo("abc", 10).transformInto[Bar]
res2: Bar = Bar("abc", 10, true)

Usage


In this section you will learn how to use Chimney example by example.

Basic transformations

When target object contains only fields present in the source object, with corresponding types, we can use shorthanded transformInto.

case class Catterpillar(size: Int, name: String)
case class Butterfly(size: Int, name: String)

val stevie = Catterpillar(5, "Steve")
val steve = stevie.transformInto[Butterfly]
// Butterfly(5, "Steve")

Nested transformations

It also works when transformation needs to be recursive, possibly involving traversal on nested collection.

case class Youngs(insects: List[Catterpillar])
case class Adults(insects: List[Butterfly])

val kindergarden = Youngs(List(Catterpillar(5, "Steve"), Catterpillar(4, "Joe")))
val highschool = kindergarden.transformInto[Adults]
// Adults(List(Butterfly(5, "Steve"), Butterfly(4, "Joe"))

We can use it as long as Chimney can recursively construct transformation for all fields of a target object. In this example transformer for List type is constructed basing on automatically derived Catterpillar ~> Butterfly mapping.

Providing missing values

Let's add a field to our Butterfly case class.

case class Butterfly(size: Int, name: String, wingsColor: String)

Now, when trying to perform the same transformation, we get compile-time error. This is naturally expected, as we don't have any data source for new wingsColor field.

val stevie = Catterpillar(5, "Steve")
val steve = stevie.transformInto[Butterfly]
// error: Chimney can't derive transformation from Catterpillar to Butterfly
//
// Butterfly
//   wingsColor: String - no field named wingsColor in source type Catterpillar
//
// Consult https://scalalandio.github.io/chimney for usage examples.
//
//        val steve = stevie.transformInto[Butterfly]
//                                        ^

In this scenario, we can use Chimney's syntax to provide a missing value. Notice that transformInto[T] is a shortcut for into[T].transform, where the latter form allow us to provide additional transformation rules.

val steve = stevie.into[Butterfly]
  .withFieldConst(_.wingsColor, "white")
  .transform
// Butterfly(5, "Steve", "white")

We can also construct a value dynamically, by providing a function.

val steve = stevie.into[Butterfly]
  .withFieldComputed(_.wingsColor, c => if(c.size > 4) "yellow" else "gray")
  .transform
// Butterfly(5, "Steve", "yellow")

Default values

Chimney also respects case classes' default values as a possible target field value source. When we want to rely on defaults, we don't need to provide values manually.

case class Butterfly(size: Int, name: String, wingsColor: String = "purple")

val steve = stevie.transformInto[Butterfly]
// Butterfly(5, "Steve", "purple")

Providing the value anyway for such case would just ignore the default from case class.

Disabling default values

It is possible to disable lookup for default values and require them to be passed explicitly, using .disableDefaultValues operation.

val steve = stevie
  .into[Butterfly]
  .disableDefaultValues
  .transform
// error: Chimney can't derive transformation from Catterpillar to Butterfly
//
// Butterfly
//   wingsColor: String - no field named wingsColor in source type Catterpillar
//
// Consult https://scalalandio.github.io/chimney for usage examples.
//
//            .transform
//            ^

Standard types

Chimney supports deriving transformers for many standard Scala types, like Options, Eithers, collection types including Lists, Vectors, Sets, Maps, Arrays and many more.

If you are interested to see how they are handled, it's recommended to explore the test suite.

Value classes

As nowadays value classes tends to be relatively widely pervasive, Chimney handles them in a special way, supporting automatic value class field extraction and wrapping.

object rich {
  case class PersonId(id: Int) extends AnyVal
  case class PersonName(name: String) extends AnyVal
  case class Person(personId: PersonId, personName: PersonName, age: Int)
}
object plain {
  case class Person(personId: Int, personName: String, age: Int)
}

val richPerson = rich.Person(PersonId(10), PersonName("Bill"), 30)
val plainPerson = richPerson.transformInto[plain.Person]
// plain.Person(10, "Bill", 30)
val richPerson2 = plainPerson.transformInto[rich.Person]
// rich.Person(PersonId(10), PersonName("Bill"), 30)

Field re-labelling

Sometimes a field only change its name. In such case you can use withFieldRenamed operation to instruct the library about performed renaming.

case class SpyGB(name: String, surname: String)
case class SpyRU(imya: String, familia: String)

val jamesGB = SpyGB("James", "Bond")

val jamesRU = jamesGB.into[SpyRU]
    .withFieldRenamed(_.name, _.imya)
    .withFieldRenamed(_.surname, _.familia)
    .transform
// SpyRU("James", "Bond")

Default option values

In case you have added an optional field to a type, wanting to write migration from old data, usually you set new optional type to None.

case class Foo(a: Int, b: String)
case class FooV2(a: Int, b: String, newField: Option[Double])

Usual approach would be to use .withFieldConst to set new field value.

Foo(5, "test")
  .into[FooV2]
  .withFieldConst(_.newField, None)
  .transform
// FooV2(5, "test", None)

At some scale this may turn out to be cumbersome. Therefore, it's possible to handle such Option field values for which we can't find counterpart in data source as None by default. You just need to enable this behavior by using .enableOptionDefaultsToNone.

Foo(5, "test")
  .into[FooV2]
  .enableOptionDefaultsToNone
  .transform
// FooV2(5, "test", None)

Advanced techniques


Custom transformations

In case the transformation is relatively complex or if for some reason you just want to bypass Chimney derivation mechanism, you can always fall back to a simple function that you can plug into the Chimney transformation.

trait Transformer[From, To] {
  def transform(src: From): To
}

The library defines a trait Transformer. You can plug that transformer in by providing implicit instance in a local context.

import io.scalaland.chimney.dsl._
import io.scalaland.chimney.Transformer

object v1 {
  case class User(id: Int, name: String, street: String, postalCode: String)
}
object v2 {
  case class Address(street: String, postalCode: String)
  case class User(id: Int, name: String, addresses: List[Address])
}

implicit val userV1toV2: Transformer[v1.User, v2.User] =
  (user: v1.User) => v2.User(
    id = user.id,
    name = user.name,
    addresses = List(v2.Address(user.street, user.postalCode))
  )

val v1Users = List(
  v1.User(1, "Steve", "Love street", "27000"),
  v1.User(2, "Anna", "Broadway", "00321")
)

val v2Users = v1Users.transformInto[List[v2.User]]
// List(
//   v2.User(1, "Steve", List(Address("Love street", "27000"))),
//   v2.User(2, "Anna", List(Address("Broadway", "00321")))
// )

Coproducts support

With Chimney you can not only transform case classes, but sealed trait hierarchies (also known as coproducts) as well. Consider two following hierarchy definitions.

sealed trait Color
object Color {
  case object Red extends Color
  case object Green extends Color
  case object Blue extends Color
}

sealed trait Channel
object Channel {
  case object Alpha extends Channel
  case object Blue extends Channel
  case object Green extends Channel
  case object Red extends Channel
}

Because of object names correspondence, we can transform Color to a Channel in a simple way.

val colRed: Color = Color.Red
val chanRed = colRed.transformInto[Channel]
// chanRed: Channel = Red

How about other way round?

chanRed.transformInto[Color]
// error: Chimney can't derive transformation from Channel to Color
//
// Color
//   can't transform coproduct instance Channel.Alpha to Color
//
// Consult https://scalalandio.github.io/chimney for usage examples.
//
//        chanRed.transformInto[Color]
//                             ^

This time we tried to transform a Channel to a Color. Notice that in this case we don't have defined case object in target hierarchy with corresponding name for case object Alpha. Wanting to keep the transformation total, we need to somehow provide a value from a target domain. We can use withCoproductInstance to do that. Let's convert any Channel.Alpha to Color.Blue.

val red = chanRed.into[Color]
  .withCoproductInstance { (_: Channel.Alpha.type) => Color.Blue }
  .transform
// red: Color = Red

val alpha: Channel = Channel.Alpha
val blue = alpha.into[Color]
  .withCoproductInstance { (_: Channel.Alpha.type) => Color.Blue }
  .transform
// blue: Color = Blue

After providing a default, Chimney can prove the transformation is total and use provided function, when it's needed.

Patchers

Chimney also supports case class patching. It is a bit different type of transformation when you hold an object of some type, but want to modify only subset of fields. Consider following example:

case class Email(address: String) extends AnyVal
case class Phone(number: Long) extends AnyVal

case class User(id: Int, email: Email, phone: Phone)
case class UserUpdateForm(email: String, phone: Long)

Let's assume you want to apply update form to existing object of type User.

val user = User(10, Email("abc@domain.com"), Phone(1234567890L))
val updateForm = UserUpdateForm("xyz@domain.com", 123123123L)

user.patchWith(updateForm)
// User(10, Email("xyz@domain.com"), Phone(123123123L))

Notice that when using patchers, we rely on standard transformers derivation rules. In this case we used value classes in the User model, but plain values in update form. Chimney was able to derive transformers for each patched field, so it was able to successfully derive a patcher.

Optional patch values

It is possible to patch using optional values of type Option[T] as long as the transformer is available for T. When the value is present, it will be transformed, otherwise it will be ignored and the field will be left as it was in the original object. Let us consider the following patch for the class User defined above:

case class UserPatch(email: Option[String], phone: Option[Phone])

Then it is possible to patch as follows:

val update = UserPatch(email = Some("updated@example.com"), phone = None)

exampleUser.patchWith(update)
//  User(10, Email("updated@example.com"), Phone(1234567890L))

The phone remained the same as in the exampleUser, while the optional e-mail string got transformed to an Email instance.

Java beans

Beside Scala case classes, Chimney supports transformation of Java beans.

Reading from Java beans

Chimney supports automatic field renaming for classes that follow Java beans naming convention. Let's assume the following classes:

class MyBean(private var id: Long,
             private var name: String,
             private var flag: Boolean) {
    def getId: Long = id
    def getName: String = name
    def isFlag: Boolean = flag
}

case class MyCaseClass(id: Long, name: String, flag: Boolean)

The conversion works if you explicitly enable it with .enableBeanGetters:

new MyBean(1L, "beanie", true)
  .into[MyCaseClass]
  .enableBeanGetters
  .transform
//  MyCaseClass(1L, "beanie", true)

Please note that Chimney matches accessor methods solely based on name and return type, and has no way of ensuring that a method named similarly to a getter is idempotent and does not actually perform side effects in its body.

Writing to Java beans

Dual to reading, Chimney supports transforming types into Java beans.

Chimney considers as bean a class that:

Chimney will then require data sources for all such setters.

class MyBean {
  private var id: Long = _
  private var name: String = _
  private var flag: Boolean = _

  def getId: Long = id
  def setId(id: Long): Unit = { this.id = id }

  def getName: String = name
  def setName(name: String): Unit = { this.name = name }

  def isFlag: Boolean = flag
  def setFlag(flag: Boolean): Unit = { this.flag = flag }
}

The conversion works if you explicitly enable it with .enableBeanSetters:

val obj = MyCaseClass(10L, "beanie", true)
val bean = obj
  .into[MyBean]
  .enableBeanSetters
  .transform

Chimney generates code equivalent to:

val bean = new MyBean
bean.setId(obj.id)
bean.setName(obj.name)
bean.setFlag(obj.flag)

Limitations

Currently it's not possible to override or provide values for missing setters.