Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: October 20, 2024
In this tutorial, we cover Chimney, a data transformation library supporting Scala 2.12, 2.13, 3.3+ on JVM, Scala.js, and Scala Native. Chimney aims to make transforming one structured immutable data type into another as painless as possible. The mechanisms available are typically used to convert structured data into a domain model. Chimney also provides a patching utility to update values in immutable nested structures but has only update optics capabilities. For official documentation, see the Chimney Cookbook.
Alternatively, Ducktape can be used for transformations between domain types. Additionally, see our article on Monocle for comprehensive optic capabilities.
Duck typing is a theoretical construct in type theory meaning types that are structurally equivalent can be substituted for each other. Scala 3 is statically typed but provides several methods for duck typing, such as Selectable, Tuple generics, and macros. Chimney provides typeclasses and macros for intuitive duck typing by means of transformation between types.
Fully isomorphic transformations between types are encoded with Transformer and the bidirectional equivalent, Iso. Partially isomorphic transformations are modeled using PartialTransformer and the bidirectional equivalent, Codec.
Optics are a set of functional abstractions to manipulate (get, set, modify, …) immutable objects. Scala provides the .copy method to modify values of immutable case classes but has otherwise lacking capabilities in the standard library. Chimney provides the Patcher typeclass to simultaneously update a set of values in an immutable nested case class.
Chimney provides utilities to generate transformations between immutable types. To add Chimney to an SBT project, we include it as a library:
// if you use Scala on JVM-only
libraryDependencies += "io.scalaland" %% "chimney" % "1.4.0"
// if you cross-compile to Scala.js and/or Scala Native
libraryDependencies += "io.scalaland" %%% "chimney" % "1.4.0"
For the most recent version, check the official quickstart page.
The Transformer type defines a translation between any two types:
trait Transformer[From, To] extends Transformer.AutoDerived[From, To]
Intuitively, any Transformer instance defines the transformation from From to To. If a transformation can throw an Exception, we use a PartialTransformer instead.
To use a Transformer, we start by defining our parameter and result domain models:
class MyType(val a: Int)
class MyOtherType(val b: String):
override def toString: String = s"MyOtherType($b)"
We can define a transformer to turn a MyType into a MyOtherType:
val transformer: Transformer[MyType, MyOtherType] =
(src: MyType) => new MyOtherType(src.a.toString)
transformer.transform(new MyType(10))
Alternatively, we can define a given instance:
given Transformer[MyType, MyOtherType] = transformer
(new MyType(10)).transformInto[MyOtherType]
Often we need to transform between two different models that represent the same information. One may model a database record and the other may model an HTTP response, with a serializer. We can generate new givens using a Transformer. Let’s demonstrate with a Serial typeclass:
trait Serial[T]:
def serial(v: T): String
given Serial[MyOtherType] with
def serial(v: MyOtherType): String = v.toString
Finally, we can use that instance to generate an instance for MyType:
given [F, T](using Serial[T], Transformer[F, T]): Serial[F] with
def serial(v: F): String =
summon[Serial[T]].serial:
summon[Transformer[F, T]].transform(v)
Chimney allows us to automatically derive transformers between different model case classes. For example, let’s define a BookDTO:
case class BookDTO(
title: String, // 1. primitive
authors: Seq[AuthorDTO], // 2. Seq collection
isbn: Option[String] // 3. Option type
)
case class AuthorDTO(name: String, surname: String)
Now we can define a more expressive domain model:
case class Book(
name: Title,
authors: List[Author],
isbn: ISBN
)
case class Title(name: String) extends AnyVal
case class Author(name: String, surname: String)
type ISBN = Option[String]
Finally, we can automatically transform a User to a UserDTO:
val book = Book(
name = Title("The Universal One"),
authors = List(Author("Walter", "Russell")),
isbn = None
)
val bookDTO: BookDTO = book.transformInto[BookDTO]
Using a library like Chimney isn’t necessary for basic duck typing transformations. The standard library provides the Selectable trait and Tuple generics.
Selectable is a trait that provides methods for structural selection to objects inherited by it:
class Record(elems: (String, Any)*) extends Selectable:
private val fields = elems.toMap
def selectDynamic(name: String): Any = fields(name)
We can use it to define a structural type:
type BookRecord = Record {
val name: Title
val authors: List[Author]
val isbn: ISBN
}
Then, we can apply a duck-typing transformation:
val naturesOpenSecret: BookRecord = Record(
"name" -> Title("Nature's Open Secret"),
"authors" -> List(Author("Rudolph", "Steiner")),
"isbn" -> Some("978-0880103930")
).asInstanceOf[BookRecord]
We can also convert a case class to a tuple and then convert it back:
val bookTuple: (Title, List[Author], ISBN) =
Tuple.fromProductTyped(book)
val bookAgain: Book =
summon[deriving.Mirror.Of[Book]].fromProduct(bookTuple)
The Iso type defines a bidirectional transformation between any two types. It’s specified using two Transformers:
final case class Iso[First, Second](first: Transformer[First, Second], second: Transformer[Second, First])
We can derive an Iso instance like this:
case class StructuredItem(uuid: java.util.UUID)
case class DomainItem(uuid: java.util.UUID)
given Iso[StructuredItem, DomainItem] = Iso.derive
The instance can then be summoned with summon[Iso[StructuredItem, DomainItem]] and either constituent transformer selected.
If we need a Transformer between two types, but transformation could throw an Exception, we use a PartialTransformer:
trait PartialTransformer[From, To] extends PartialTransformer.AutoDerived[From, To]
Instead of returning an output type, a PartialTransformer returns partial.Result[To], which is either a Value[To] or Errors:
sealed trait Result[+A]
final case class Value[A](value: A) extends Result[A]
final case class Errors(errors: NonEmptyErrorsChain) extends Result[Nothing]
The Value class returns a successful transformation and the Errors class accumulates validation errors using an internal data structure called a NonEmptyErrorsChain:
sealed abstract class NonEmptyErrorsChain extends Iterable[partial.Error]
The NonEmptyErrorsChain has inherited Iterable properties from the standard library. The partial.Error class contains an ErrorMessage and a Path:
final case class Error(message: ErrorMessage, path: Path = Path.Empty)
The ErrorMessage type is an enumeration with an EmptyValue case object, and NotDefinedAt, StringMessage, and ThrowableMessage case class members.
sealed trait ErrorMessage
object ErrorMessage:
case object EmptyValue extends ErrorMessage
final case class NotDefinedAt(arg: Any) extends ErrorMessage
final case class StringMessage(message: String) extends ErrorMessage
final case class ThrowableMessage(throwable: Throwable) extends ErrorMessage
The Path class is for locating fields in a nested data structure:
final case class Path(private val elements: List[PathElement])
Finally, the PathElement type is an enumeration:
sealed trait PathElement
object PathElement:
final case class Accessor(name: String) extends PathElement
final case class Index(index: Int) extends PathElement
final case class MapValue(key: Any) extends PathElement
final case class MapKey(key: Any) extends PathElement
The Accessor, Index, MapValue, and MapKey case classes allow us to locate any contentious data points in a failed transformation.
We can define a PartialTransformer with a function:
val fn: Int => Boolean =
case 0 => false
case 1 => true
case i => throw Exception(s"Provided integer invalid: $i")
given PartialTransformer[Int, Boolean] =
PartialTransformer.fromFunction(fn)
Finally, we can apply the PartialTransformer with the transformIntoPartial implicit method:
val result: Result[Boolean] = 0.transformIntoPartial[Boolean]
If an output of partial transformation can be pattern matched to match successful and failed cases:
result match
case Result.Value(bool) => println(bool)
case Result.Errors(errs) => println(errs)
Any successful output is a Result.Value and exceptions thrown are caught in Result.Errors.
The Codec type defines a bidirectional transformation that must succeed in one direction and may fail in the other direction. It’s defined using a Transformer and a PartialTransformer:
final case class Codec[Domain, Dto](encode: Transformer[Domain, Dto], decode: PartialTransformer[Dto, Domain])
Then, we can define a Codec instance like this:
case class Domain(a: Int, b: String)
case class Dto(b: Option[String], a: Option[Int])
given Codec[Domain, Dto] = Codec.derive
The instance can then be summoned with summon[Codec[Domain, Dto]] and either constituent transformer selected.
The Patcher type defines optics-based update behavior for nested case classes:
trait Patcher[A, Patch] extends Patcher.AutoDerived[A, Patch]
A is the type we wish to patch and Patch is the type we use to patch it.
We define the Patch parameters with another case class called UpdateISBN:
case class UpdateISBN(isbn: ISBN)
Then, we need a Book to patch and an UpdateISBN:
val book = Book(
name = Title("Synergetics"),
authors = List(Author("Buckminster", "Fuller")),
isbn = None
)
val isbnUpdateForm = UpdateISBN(
isbn = Some("978-0206532048")
)
Finally, we automatically patch any User with a UserUpdateForm:
val hardcover: Book = book.patchUsing(isbnUpdateForm)
Alternatively, an immutable copy of a case class is with computed with the .copy method automatically generated for case classes by the Scala compiler. We trivially create a new immutable copy of any instance:
val softcover: Book =
book.copy(
authors = List(
Author("Buckminster", "Fuller"),
Author("Edmund", "Applewhite")
),
isbn = Some("978-0020653202")
)
Chimney is configured by the TransformerConfiguration and PatcherConfiguration classes to configure the way transformers and patchers are derived. We find the specific configuration flags are documented in the Chimney Cookbook.
We set the configurations with givens:
transparent inline given TransformerConfiguration[?] =
TransformerConfiguration.default.enableMethodAccessors.enableMacrosLogging
transparent inline given PatcherConfiguration[?] =
PatcherConfiguration.default.ignoreNoneInPatch.enableMacrosLogging
In this article, we’ve introduced Chimney, a library for reducing boilerplate in Scala. We’ve covered the installation process; the Transformer, Iso, PartialTransformer, Codec, and Patcher type classes; and configuration in Scala 3. Additionally, we touched on suitable standard library alternatives.