Authors Top

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

1. Introduction

Configurations are an essential part of a software application. However, parsing and processing configuration files can be a tedious process involving a lot of boiler-plate code. This approach is error-prone and repetitive. In this tutorial, we’ll look at PureConfig, a small and effective Scala library for working with configuration files.

2. Advantages of PureConfig

Some of the advantages of using PureConfig are:

  • No boiler-plate code is needed to read config files
  • Support for multiple formats such as properties files, JSON, and HOCON
  • Ability to support custom types
  • Simple and intuitive APIs

3. Setup

To use PureConfig in the project, we’ll first add the dependency to our build.sbt file:

libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.17.1"

Now, we can add the import statements which can support most of the basic operations:

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

4. Loading Configuration Files

4.1. Read From the application.conf File

Let’s first define a simple config in our application.conf file:

notification-url = "http://mynotificationservice.com/push"
params = "status=completed"

Next, we’ll define the case class to hold this configuration:

final case class NotificationConfig(notificationUrl:String, params:String)

Note that the key in the config file is using kebab-case, whereas the case class follows the camel case. This is the default standard in PureConfig. However, it’s possible to customize this format. Now, let’s load the config file:

val notificationConfResult: ConfigReader.Result[NotificationConfig] = ConfigSource.load[NotificationConfig]

ConfigReader.Result is an alias for Either[ConfigReaderFailures, NotificationConfig]. We can access the configuration value from the result as:

val params = notificationConfResult.right.get.params
params shouldBe "status=completed"

We can also read and load the config values from a specific node in the application.conf file, such as:

kafka {
    port = 8090
    bootstrap-server = "kafka.mydomain.com"
    protocol = "https"
    timeout = 2s
}

To do this, we’ll define the corresponding case class to hold the config values:

final case class Port(number: Int) extends AnyVal
final case class KafkaConfig(
  bootstrapServer: String,
  port: Port,
  protocol: String,
  timeout: FiniteDuration
)

We can now load the config values as:

val kafkaConf = ConfigSource.default.at("kafka").load[KafkaConfig]

This will load the config values at the kafka node from the application.conf file into KafkaConfig case class.

4.2. Read From a Custom Config File

PureConfig can also read config values from custom files. To load configuration values from a file named notification.conf, we can use:

val notificationConf = ConfigSource.resources("notification.conf").load[NotificationConfig]

4.3. Read From String Content

If the configuration values are available as a String, we can load them into the config class as:

val notificationConfStr = ConfigSource.string("""{"notification-url": "https://newURL", "params":"status=pending"}""")

4.4. Throw Exception on Failures

By default, PureConfig doesn’t throw any exceptions if it fails to read configurations. Instead, it will return the Either result with the Left value containing the error information. However, in some cases, it is better to fail immediately by throwing an exception. We can do that by using the loadOrThrow method during config load:

val conf = ConfigSource.string("""{"notification-u": "https://wrongURL", "params":"status=pending"}""").loadOrThrow[NotificationConfig]

5. Customizations

5.1. Customize Config Field Format

By default, PureConfig expects the configuration keys in kebab-case and case class fields in camel-case. We can customize the default behavior by providing “hints”. PureConfig supports other formats such as Pascal Case, Snake Case, and Screaming Snake Case. We can define the required formats as implicit hints:

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

Now, PureConfig will expect both config keys and case class fields to be in Camel Case.

5.2. Customize Date Format

We can define the required date format in the configurations using converters:

implicit val localDateConvert = localDateConfigConvert(DateTimeFormatter.ISO_DATE)
implicit val localDateTimeConvert = localDateTimeConfigConvert(DateTimeFormatter.ISO_DATE_TIME)

6. Using Custom Types

PureConfig supports a wide variety of types for the configuration values. It can read types such as String, Int, Double, FiniteDuration, UUID, and LocalDate. Apart from these, PureConfig already provides integrations for other commonly used types such as Enumeratum, JodaTime, and Circe.

6.1. Use PureConfig Integration

Let’s look at how we can use Enumeratum type for config. First, we need to add the dependency for the integration library:

libraryDependecies += "com.github.pureconfig" %% "pureconfig-enumeratum" % "0.17.1"

Then we can create the required config:

app-name = "baeldung"
env = Prod
baseDate = "2022-01-02"

We want the field env to have Enumeratum values as Prod and Test. Next, let’s define the Enumeratum types:

sealed trait Env extends EnumEntry
object Env extends Enum[Env] {
  case object Prod extends Env
  case object Test extends Env
  override val values = findValues
}
final case class BaseAppConfig(appName: String, baseDate: LocalDate, env: Env)

Now, we can use load the configuration values:

import pureconfig.module.enumeratum._
implicit def hint[A] = ProductHint[A](ConfigFieldMapping(CamelCase, CamelCase))
val baseConfig = ConfigSource.default.load[BaseAppConfig]

6.2. Use Custom Type

We can also use our custom types in PureConfig. Let’s see how we can handle a sealed trait ADT for config. First, we can define the ADT structure:

sealed trait Protocol
object Protocol {
  case object Http extends Protocol
  case object Https extends Protocol
}

Then, we need to define a reader for the sealed trait:

implicit val protocolConvert: ConfigReader[Protocol] = deriveEnumerationReader[Protocol]

As a result, PureConfig will be able to handle the type Http or Https in the configurations.

7. Conclusion

In this tutorial, we looked at handling configurations using PureConfig. As always, the code samples used here are available over on GitHub.

Authors Bottom

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

Comments are closed on this article!