Case Classes

PureConfig has to assume some conventions and behaviors when deriving ConfigReader instances for case classes:

  • How do keys in config objects map to field names of the case class?
  • Are unknown keys allowed in the config object?
  • Should default values in case class fields be applied when its respective config key is missing?

By default, PureConfig:

  • expects config keys to be written in kebab case (such as my-field) and the associated field names are written in camel case (such as myField);
  • allows unknown keys;
  • uses the default values when a key is missing.

All of these assumptions can be overridden by putting an implicit ProductHint in scope - an object that “hints” PureConfig on how to best derive converters for products.

Field mappings

In case the naming convention you use in your configuration files differs from the default one, PureConfig allows you to define the mappings to use. A mapping between different naming conventions is done using a ConfigFieldMapping object, with which one can construct a ProductHint. The ConfigFieldMapping trait has a single apply method that maps field names in Scala objects to field names in the source configuration file.

For instance, here’s a contrived example where the configuration file has all keys in upper case and we’re loading it into a type whose fields are all in lower case. First, define a ProductHint instance in implicit scope:

import pureconfig._
import pureconfig.generic.ProductHint

case class SampleConf(foo: Int, bar: String)

implicit val productHint = ProductHint[SampleConf](new ConfigFieldMapping {
  def apply(fieldName: String) = fieldName.toUpperCase

Then load a config:

ConfigSource.string("{ FOO: 2, BAR: two }").load[SampleConf]
// res0: ConfigReader.Result[SampleConf] = Right(SampleConf(2, "two"))

PureConfig provides a way to create a ConfigFieldMapping by defining the naming conventions of the fields in the Scala object and in the configuration file. Some of the most used naming conventions are supported directly in the library:

  • CamelCase (examples: camelCase, useMorePureconfig);
  • SnakeCase (examples: snake_case, use_more_pureconfig);
  • ScreamingSnakeCase (examples: SCREAMING_SNAKE_CASE, USE_MORE_PURECONFIG);
  • KebabCase: (examples: kebab-case, use-more-pureconfig);
  • PascalCase: (examples: PascalCase, UseMorePureconfig).

You can use the apply method of ConfigFieldMapping that accepts the two naming conventions (for the fields in the Scala object and for the fields in the configuration file, respectively). A common use case is to have both your field names and your configuration files in camelCase. In order to support it, you can make sure the following implicit is in scope before loading or writing configuration files:

implicit def hint[A] = ProductHint[A](ConfigFieldMapping(CamelCase, CamelCase))

Default field values

If a case class has a default argument and the underlying configuration is missing a value for that field, then by default PureConfig will happily create an instance of the class, loading the other values from the configuration.

For example, with this setup:

import pureconfig._
import pureconfig.generic.ProductHint
import scala.concurrent.duration._
import scala.language.postfixOps

case class Holiday(where: String = "last resort", howLong: Duration = 7 days)

We can load configurations using default values:

// Defaulting `where`
ConfigSource.string("{ how-long: 21 days }").load[Holiday]
// res2: ConfigReader.Result[Holiday] = Right(Holiday("last resort", 21 days))

// Defaulting `howLong`
ConfigSource.string("{ where: Zürich }").load[Holiday]
// res3: ConfigReader.Result[Holiday] = Right(Holiday("Zürich", 7 days))

// Defaulting both arguments
// res4: ConfigReader.Result[Holiday] = Right(Holiday("last resort", 7 days))

// Specifying both arguments
ConfigSource.string("{ where: Texas, how-long: 3 hours }").load[Holiday]
// res5: ConfigReader.Result[Holiday] = Right(Holiday("Texas", 3 hours))

A ProductHint can make the conversion fail if a key is missing from the config regardless of whether a default value exists or not:

implicit val hint = ProductHint[Holiday](useDefaultArgs = false)
ConfigSource.string("{ how-long: 21 days }").load[Holiday]
// res6: ConfigReader.Result[Holiday] = Left(
//   ConfigReaderFailures(
//     ConvertFailure(KeyNotFound("where", Set()), Some(ConfigOrigin(String)), ""),
//     WrappedArray()
//   )
// )

Unknown keys

By default, PureConfig ignores keys in the config that do not map to any case class field, leading to potential bugs due to misspellings:

ConfigSource.string("{ wher: Texas, how-long: 21 days }").load[Holiday]
// res8: ConfigReader.Result[Holiday] = Right(Holiday("last resort", 21 days))

With a ProductHint, one can tell the converter to fail if an unknown key is found:

implicit val hint = ProductHint[Holiday](allowUnknownKeys = false)
// hint: ProductHint[Holiday] = ProductHintImpl(<function1>, true, false)

ConfigSource.string("{ wher: Texas, how-long: 21 days }").load[Holiday]
// res9: ConfigReader.Result[Holiday] = Left(
//   ConfigReaderFailures(
//     ConvertFailure(UnknownKey("wher"), Some(ConfigOrigin(String)), "wher"),
//     List()
//   )
// )

Missing keys

The default behavior of ConfigReaders that are derived in PureConfig is to return a KeyNotFound failure when a required key is missing unless its type is an Option, in which case it is read as a None.

Consider this configuration:

import pureconfig._

case class Foo(a: Int)
case class FooOpt(a: Option[Int])

Loading a Foo results in a Left because of missing keys, but loading a FooOpt produces a Right:

// res11: ConfigReader.Result[Foo] = Left(
//   ConfigReaderFailures(
//     ConvertFailure(
//       KeyNotFound("a", Set()),
//       Some(ConfigOrigin(empty config)),
//       ""
//     ),
//     WrappedArray()
//   )
// )
// res12: ConfigReader.Result[FooOpt] = Right(FooOpt(None))

However, if you want to allow your custom ConfigReaders to handle missing keys, you can extend the ReadsMissingKeys trait. For ConfigReaders extending ReadsMissingKeys, a missing key will issue a call to the from method of the available ConfigReader for that type with a cursor to an undefined value.

Under this setup:

implicit val maybeIntReader = new ConfigReader[Int] with ReadsMissingKeys {
  override def from(cur: ConfigCursor) =
    if (cur.isUndefined) Right(42) else ConfigReader[Int].from(cur)

You can load an empty configuration and get a Right:

// res13: ConfigReader.Result[Foo] = Right(Foo(42))