Sealed Families

In order for PureConfig to disambiguate between different options of a sealed family of case classes, it must read and write additional information in configurations. By default it uses the additional field type, encoding the concrete class represented in the configuration.

Given an AnimalConf sealed trait:

import pureconfig._
import pureconfig.generic.auto._

sealed trait AnimalConf
case class DogConf(age: Int) extends AnimalConf
case class BirdConf(canFly: Boolean) extends AnimalConf

This will load a DogConf instance:

ConfigSource.string("{ type: dog-conf, age: 4 }").load[AnimalConf]
// res0: ConfigReader.Result[AnimalConf] = Right(DogConf(4))

For sealed families, PureConfig provides a way to customize the conversion without replacing the default ConfigReader. By putting in scope an instance of CoproductHint for that sealed family, we can customize how the disambiguation is made. For example, if type clashes with one of the fields of a case class option, we can use another field.

First, define a CoproductHint in implicit scope:

import pureconfig.generic.FieldCoproductHint

implicit val animalConfHint = new FieldCoproductHint[AnimalConf]("kind")

Then load the config:

ConfigSource.string("{ kind: dog-conf, age: 4 }").load[AnimalConf]
// res1: ConfigReader.Result[AnimalConf] = Right(DogConf(4))

FieldCoproductHint can also be adapted to write class names in a different way. First, define a new FieldCoproductHint in implicit scope:

implicit val animalConfHint = new FieldCoproductHint[AnimalConf]("type") {
  override def fieldValue(name: String) = name.dropRight("Conf".length)
}

Then load the config:

ConfigSource.string("{ type: Bird, can-fly: true }").load[AnimalConf]
// res2: ConfigReader.Result[AnimalConf] = Right(BirdConf(true))

If you encode enumerations using sealed traits of case objects, you can use the deriveEnumerationReader method from the pureconfig.generic.semiauto package to derive ConfigReader instances for your sealed trait.

import pureconfig._
import pureconfig.generic.auto._
import pureconfig.generic.semiauto._

sealed trait Season
case object Spring extends Season
case object Summer extends Season
case object Autumn extends Season
case object Winter extends Season

implicit val seasonConvert: ConfigReader[Season] = deriveEnumerationReader[Season]

case class MyConf(list: List[Season])

We can load seasons by specifying them by class name:

ConfigSource.string("{ list: [spring, summer, autumn, winter] }").load[MyConf]
// res4: ConfigReader.Result[MyConf] = Right(
//   MyConf(List(Spring, Summer, Autumn, Winter))
// )

By default, enumerations will be encoded as strings with the kebab-case representation of the class name, but that behavior can be overridden by specifying a different transformation function.

sealed trait Color
case object RainyBlue extends Color
case object SunnyYellow extends Color

implicit val colorReader: ConfigReader[Color] = deriveEnumerationReader[Color](ConfigFieldMapping(PascalCase, SnakeCase))

case class ColorList(colors: List[Color])
ConfigSource.string("{ colors: [rainy_blue, sunny_yellow] }").load[ColorList]
// res5: ConfigReader.Result[ColorList] = Right(
//   ColorList(List(RainyBlue, SunnyYellow))
// )