Chimney
The battle-tested Scala library for data transformations
Removing boilerplate since 2017.
What does it mean? Imagine you'd have to convert between this Protobuf-like definitions:
Example
// file: dto.scala - part of the demo
//> using scala 3.3.3
case class UserDTO(
name: String, // 1. primitive
addresses: Seq[AddressDTO], // 2. Seq collection
recovery: Option[RecoveryMethodDTO] // 3. Option type
)
case class AddressDTO(street: String, city: String)
// 4. ADT is not flat - each oneOf message created 2 case classes
sealed trait RecoveryMethodDTO
object RecoveryMethodDTO {
case class Phone(value: PhoneDTO) extends RecoveryMethodDTO
case class Email(value: EmailDTO) extends RecoveryMethodDTO
}
case class PhoneDTO(number: String)
case class EmailDTO(email: String)
and this domain model:
Example
// file: domain.scala - part of the demo
case class User(
name: Username, // 1. value class
addresses: List[Address], // 2. List collection
recovery: RecoveryMethod // 3. non-Option type
)
case class Username(name: String) extends AnyVal
case class Address(street: String, city: String)
// 4. flat enum
enum RecoveryMethod:
case Phone(number: String)
case Email(email: String)
- you'd have to wrap and unwrap
AnyVal
- you'd have to convert a collection
- in transformation in one way you'd have to wrap with
Option
, and on the way back handleNone
- in one transformation you'd have to manually flatten ADT, and on the way back you have had to unflatten it
Can you imagine all the code you'd have to write? For now! And the necessity to carefully update when the model changes? The silly mistakes with using the wrong field you'll inevitably make while copy-pasting a lot of repetitive, boring and dumb code?
From now on, forget about it! Encoding domain object with an infallible transformation, like a total function?
Example
// file: conversion.sc - part of the demo
//> using dep io.scalaland::chimney::1.4.0
//> using dep com.lihaoyi::pprint::0.9.0
import io.scalaland.chimney.dsl._
pprint.pprintln(
User(
Username("John"),
List(Address("Paper St", "Somewhere")),
RecoveryMethod.Email("john@example.com")
).transformInto[UserDTO]
)
// expected output:
// UserDTO(
// name = "John",
// addresses = List(AddressDTO(street = "Paper St", city = "Somewhere")),
// recovery = Some(value = Email(value = EmailDTO(email = "john@example.com")))
// )
Curious about the generated code?
// macro outputs code like this (reformatted a bit for readability):
final class $anon() extends Transformer[User, UserDTO] {
def transform(src: User): UserDTO = {
val user: User = src
new UserDTO(
user.name.name,
user.addresses.iterator
.map[AddressDTO](((param: Address) => {
val `user.addresses`: Address = param
new AddressDTO(`user.addresses`.street, `user.addresses`.city)
}))
.to[Seq[AddressDTO]](Seq.iterableFactory[AddressDTO]),
Option[RecoveryMethod](user.recovery).map[RecoveryMethodDTO](((`param₂`: RecoveryMethod) => {
val recoverymethod: RecoveryMethod = `param₂`
(recoverymethod: RecoveryMethod) match {
case phone: RecoveryMethod.Phone =>
new RecoveryMethodDTO.Phone(new PhoneDTO(phone.number))
case email: RecoveryMethod.Email =>
new RecoveryMethodDTO.Email(new EmailDTO(email.email))
}
}))
)
}
}
(new $anon(): Transformer[User, UserDTO])
Done! Decoding Protobuf into domain object with a fallible transformation, like a partial-function but safer?
Example
// file: conversion.sc - part of the demo
//> using dep io.scalaland::chimney::1.4.0
//> using dep com.lihaoyi::pprint::0.9.0
import io.scalaland.chimney.dsl._
pprint.pprintln(
UserDTO(
"John",
Seq(AddressDTO("Paper St", "Somewhere")),
Option(RecoveryMethodDTO.Email(EmailDTO("john@example.com")))
)
.transformIntoPartial[User]
.asEither
.left
.map(_.asErrorPathMessages)
)
// expected output:
// Right(
// value = User(
// name = Username(name = "John"),
// addresses = List(Address(street = "Paper St", city = "Somewhere")),
// recovery = Email(email = "john@example.com")
// )
// )
pprint.pprintln(
UserDTO(
"John",
Seq(AddressDTO("Paper St", "Somewhere")),
None
)
.transformIntoPartial[User]
.asEither
.left
.map(_.asErrorPathMessages)
)
// expected output:
// Left(value = List(("recovery", EmptyValue)))
Curious about the generated code?
// macro outputs code like this (reformatted a bit for readability):
final class $anon() extends PartialTransformer[UserDTO, User] {
def transform(src: UserDTO, failFast: Boolean): partial.Result[User] = {
val userdto: UserDTO = src
userdto.recovery
.map[partial.Result[RecoveryMethod]](((param: RecoveryMethodDTO) => {
val recoverymethoddto: RecoveryMethodDTO = param
partial.Result.Value[RecoveryMethod]((recoverymethoddto: RecoveryMethodDTO) match {
case phone: RecoveryMethodDTO.Phone =>
new RecoveryMethod.Phone(phone.value.number)
case email: RecoveryMethodDTO.Email =>
new RecoveryMethod.Email(email.value.email)
})
}))
.getOrElse[partial.Result[RecoveryMethod]](partial.Result.fromEmpty[RecoveryMethod])
.prependErrorPath(partial.PathElement.Accessor("recovery"))
.map[User](((`param₂`: RecoveryMethod) => {
val recoverymethod: RecoveryMethod = `param₂`
new User(
new Username(userdto.name),
userdto.addresses.iterator
.map[Address](((`param₃`: AddressDTO) => {
val `userdto.addresses`: AddressDTO = `param₃`
new Address(`userdto.addresses`.street, `userdto.addresses`.city)
})
)
.to[List[Address]](List.iterableFactory[Address]),
recoverymethod
)
}))
}
}
(new $anon(): PartialTransformer[UserDTO, User])
Also done! And if a field cannot be converted, you'll get the path to the problematic value!
Now, visit the quick start section to learn how to get Chimney and the move to the supported transformations section to learn about a plethora of transformations supported out of the box and even more enabled with easy customizations!
Tip
If you have any questions don't forget to look at cookbook for new usage ideas, troubleshooting for solving problems and our GitHub discussions page!