Baeldung Pro – Scala – NPI EA (cat = Baeldung on Scala)
announcement - icon

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.

1. Introduction

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.

2. Theoretical Background

2.1. Duck Typing

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.

2.2. Optics

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.

3. Project Setup

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.

4. Transformer

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.

4.1. Using Transformer

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]

4.2. Transitive Given Instances

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)

4.3. Automatic Case Class Transformation

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]

4.4. Standard Library Alternatives

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)

5. Iso

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.

6. PartialTransformer

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.

6.1. Using PartialTransformer

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.

7. Codec

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.

8. Patcher

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.

8.1. Using Patcher

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)

8.2. Standard Library Alternative

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")
  )

9. Configuration

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

10. Conclusion

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.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.