Loading Configs in Scala using PureConfig

Loading Configs in Scala using PureConfig

Introduction

Configurations are an important part of any application. Different configurations are loaded and processed for the application to work properly. Many a times, repetitive and boilerplate code is needed to handle and load the configurations. Also, we need to write code for handling types of each config. If all these boilerplate code can be auto-generated, it makes handling and maintaining configurations easy. And that is what PureConfig does.

We can create the required case classes to hold the configurations. Pureconfig can then load the config from the files to these config classes. Pureconfig can handle HOCON, JSON and Properties files by default.

Setup

To use pureconfig, add the dependency as:

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

Types

Pureconfig can handle most of the types automatically. We can write custom converters for non supported types.

Load config from application.conf

Let's try to load a simple configuration from application.conf file. For example, let's say that we have database config:

url = "postgresql://localhost:5432"
database-name = "configs"

Firstly, we need to create a case class corresponding to the config:

final case class DatabaseConfig(url:String, databaseName:String)

Note that, by default the pureconfig expects the configuration fields in kebab case(e.g: database-name) and the case class fields in camel case(e.g: databaseName). We can configure different formats by providing hints to the pureconfig.

Now, let's load the config:

val dbConfResult: ConfigReader.Result[DatabaseConfig] = ConfigSource.load[DatabaseConfig]

ConfigReader.Result[DatabaseConfig] is just an alias for Either[ConfigReaderFailures, DatabaseConfig]. It can be handled like any other Either values.

dbConfResult.right.get.databaseName shouldBe "configs"

Load config at a specific node

We can also load config from a specific config root. Let's define the config in application.conf as:

http {
    port = 8090
    host = "localhost"
    protocol = "https"
    default-timeout = 8s
}

Next, we need to create case classes to hold this configurations. Note that we have different types of values such as Int, String and FiniteDuration.

val httpConf = ConfigSource.default.at("http").load[HttpConfig]

Load config from a custom file

We can also load the configs from a custom file. Let's move the database configurations to a separate file database.conf. Now, let's load the config from that file:

val dbConf = ConfigSource.resources("database.conf").load[DatabaseConfig]

Load config from String

Pureconfig can parse a string value to the relevant config classes. We can use the source as string instead of resources for this:

val dbConfStr = ConfigSource.string("""{"database-nam": "strDB", "url":"mysql://localhost"}""")

Customise config format

By default, Pureconfig expects the config keys in kebab case and the case class fields as camelCase. We can override this by providing an implicit hint. To use camel case for both config and case class fields, we can define hint as:

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

Apart from CamelCase, Pureconfig provides other formats such as PascalCase, SnakeCase, ScreamingSnakeCase and KebabCase.

Use custom types

Apart from the default types, we can extend Pureconfig to support custom types. Pureconfig already has integrations for many common libraries such as JodaTime, Enumeratum, Circe, and so on. Let's look at how we can use Enumeratum type in configurations. Firstly, we need to define the configurations:

name = "mainApp"
env = Prod
startDate = "2022-01-02"

We need to define the enums for env in Enumeratum:

sealed trait Env extends EnumEntry
object Env extends Enum[Env] {
  case object Prod extends Env
  case object Test extends Env
  override val values = findValues
}

Now, let's define the case class to hold the configuration values:

final case class BaseAppConfig(name: String, startDate: LocalDate, env: Env)

To use Enumeratum integration, we need to import:

import pureconfig.module.enumeratum._

Now, we can load the config as:

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

Throw Exception on Failures

Sometimes, it is required to throw exception and fail the application start if configurations are not proper. We can do that by using the method loadOrThrow while loading the config:

val conf = ConfigSource.string("""{"databaseName": "strDB", "url":"mysql://localhost"}""").loadOrThrow[DatabaseConfig]

Conclusion

In this short tutorial, we looked at how we can use Pureconfig to load the configuration files. The sample code used here is available over on GitHub.