Mike Slinn

HOCON Configuration

— Draft —

Published 2014-03-21. Last modified 2018-04-09.
Time to read: 9 minutes.

HOCON configuration is a flexible and standards-compliant way to provide configuration data to any Java or Scala application. This lecture will be of interest if you intend to work with any JVM-based application that has configuration data.

The sample code for this lecture can be found in courseNotes/src/main/scala/ConfigFun.scala.

The HOCON Config utility is a flexible and standards-compliant way to provide configuration data to any Java or Scala application. The utility classes are written in Java and the JavaDoc is here.

This Java-based configuration utility is commonly used for specifying and parsing Scala configuration data. Config can read Java properties, YAML, JSON, and a human-friendly JSON superset called HOCON. Config does not currently have a standardized Scala wrapper, although some people have created their own.

From the point of view of your application, the file format of your configuration data makes no difference. What’s more, if a combination of data file formats is used, they can easily be merged without regard to their format on disk.

We will use the sbt console command below instead of the Scala REPL because we want to be able to access the configuration data files on the application classpath.

Working with Java Properties Files

A Java properties file called courseNotes/src/main/resources/demo.properties has been provided in the courseNotes project. It looks like this:

demo.properties
string1=Hello from demo.properties
int1=42
double1=123.45
elapsedTime=1 day
bytes1=2K
bytes2=3G

We can use Config from the SBT console to obtain strongly typed configuration data from this file. First we start the sbt console and import ConfigFactory:

Shell
$ sbt console
Loading /usr/share/sbt/bin/sbt-launch-lib.bash
[info] Loading global plugins from /home/mslinn/.sbt/0.13/plugins
[info] Loading project definition from /home/mslinn/work/course_scala_intermediate_code/courseNotes/project
[info] Updating {file:/home/mslinn/work/course_scala_intermediate_code/courseNotes/project/}coursenotes-build...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Set current project to IntermediateScalaCourse (in build file:/home/mslinn/work/course_scala_intermediate_code/courseNotes/)
import java.io.File
import java.net.URL
import scala.sys.process._
Welcome to Scala version 2.11.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_60).
Type in expressions to have them evaluated.
Type :help for more information.
scala>
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory

Next we load demo.properties from the classpath and obtain a Java com.typesafe.config.Config object; this object will contain all the values stored in the file. Notice that I followed the invocation of parseResources with resolve. You should get in the habit of doing that because the resolve method performs variable evaluation; if you do not call resolve, and there are variables to resolve in the configuration, the values returned will be incorrect.

Scala REPL
scala> val confDemo = ConfigFactory.parseResources( "demo.properties ")
                                   .resolve
confDemo: com.typesafe.config.Config = Config(SimpleConfigObject({ "bytes1 ": "2K ", "bytes2 ": "3G ", "double1 ": "123.45 ", "elapsedTime ": "1 day ", "int1 ": "42 ", "string1 ": "Hello from demo.properties "})) 

Typed Conversions

You can obtain a property’s value after converting to a desired type using the appropriate getter. An exception is thrown if the conversion fails.

Scala REPL
scala> val string1 = confDemo.getString( "string1 ")
string1: String = Hello from demo.properties
scala>
val int1 = confDemo.getInt( "int1 ") int1: Int = 42
scala>
val string = confDemo.getString( "int1 ") string: String = 42
scala>
val double1 = confDemo.getDouble( "double1 ") double1: Double = 123.45

Durations

Config can also perform time conversions while parsing using java.util.concurrent.TimeUnit.

Scala REPL
scala> import java.util.concurrent.TimeUnit"._
import java.util.concurrent.TimeUnit._
scala>
val elapsedDays = confDemo.getDuration( "elapsedTime ", DAYS) elapsedDays: Long = 1
scala>
val elapsedHours = confDemo.getDuration( "elapsedTime ", HOURS) elapsedHours: Long = 24
scala>
val elapsedSeconds = confDemo.getDuration( "elapsedTime ", SECONDS) elapsedSeconds: Long = 86400

Size Conversions

Byte conversions are also supported from various size abbreviations, including K (kilo), M (mega), G (giga), T(tera), P (peta), E (exa), Z (zetta) and Y (yotta).

Scala REPL
scala> val bytes1 = confDemo.getBytes( "bytes1 ")
bytes1: Long = 2048
scala>
val bytes2 = confDemo.getBytes( "bytes2 ") bytes2: Long = 3221225472

An exception is thrown if you use an abbreviation that results in Long overflow, such as P (peta), E (exa), Z (zetta) or Y (yotta).

Stacking Properties Files Using withFallback

Multiple withFallback invocations can be chained together so the Config files are stacked, or layered. Config will load them all from right to left, overwriting keys with values as they are encountered. Although I originally drew this diagram to describe Akka configuration, it can equally well apply to your program.

The conventions used by Akka and Play are the same: a file called reference.conf embedded in the framework to provide default values, and you can provide a file called application.conf to override selected values.

You could take this convention a couple of steps further, whereby each library you write provides its own library.conf to set default configuration values for that library, and you also override specific values in test suites with a Config set from a String.

One of the exercises in this lecture will provide you with a practical example of stacking Configs.

I provided a second Java properties file, courseNotes/src/main/resources/demo2.properties. This file contains some duplicate keys that are also found in demo.properties.

demo2.properties
string1=Hello from demo2.properties
int1=13
double2=999.99

Config can overlay the two files by chaining parseResourses.withFallback. This causes demo2.properties to define the key/value pairs; any keys defined in demo.properties that are not also specified in demo2.properties will be made available. Again, I call resolve to ensure that any variables have values substituted for references.

Scala REPL
scala> val confDemo2 = ConfigFactory.parseResources( "demo2.properties ")
                                    .withFallback(confDemo)
                                    .resolve
confDemo2: com.typesafe.config.Config = Config(SimpleConfigObject({ "bytes1 ": "2K ", "bytes2 ": "3G ", "double1 ": "123.45 ", "double2 ": "999.99 ", "elapsedTime ": "1 day ", "int1 ": "13 ", "string1 ": "Hello from demo2.properties "}))
scala>
val string1b = confDemo2.getString( "string1 ") string1b: String = Hello from demo2.properties
scala>
val int1b = confDemo2.getInt( "int1 ") int1b: Int = 13
scala>
val double1b = confDemo2.getDouble( "double1 ") double1b: Double = 123.45
scala>
val double2 = confDemo2.getDouble( "double2 ") double2: Double = 999.99

Reading JSON Files

The JSON file courseNotes/src/main/resources/demo.json has been provided and looks like this:

demo.json
{
     "firstName ":  "Jane ",
     "lastName ":  "Smith ",
     "isAlive ": true,
     "age ": 25,
     "height_cm ": 147.64,
     "address ": {
         "streetAddress ":  "21 2nd Street ",
         "city ":  "New York ",
         "state ":  "NY ",
         "postalCode ":  "10021-3100 "
    },
     "phoneNumbers ": [
        {  "type ":  "home ",  "number ":  "212 555-1234 " },
        {  "type ":  "office ",   "number ":  "646 555-4567 " }
    ]
}

Config can read this file using the same methods as was used for Java properties. Notice that demo.json is parsed into a Config object, and the combined contents of the two Java properties files are also added to the resulting Config object.

Scala REPL
scala> val confDemo3 = ConfigFactory.parseResources( "demo.json ")
                                    .withFallback(confDemo2)
confDemo3: com.typesafe.config.Config = Config(SimpleConfigObject({ "address ":{ "city ": "New York ", "postalCode ": "10021-3100 ", "state ": "NY ", "streetAddress ": "21 2nd Street "}, "age ":25, "bytes1 ": "2K ", "bytes2 ": "3G ", "double1 ": "123.45 ", "double2 ": "999.99 ", "elapsedTime ": "1 day ", "firstName ": "Jane ", "height_cm ":147.64, "int1 ": "13 ", "isAlive ":true, "lastName ": "Smith ", "phoneNumbers ":[{ "number ": "212 555-1234 ", "type ": "home "},{ "number ": "646 555-4567 ", "type ": "office "}], "string1 ": "Hello from demo2.properties "})) 

We can see that all of the key/value pairs from the two Java properties files are available.

Scala REPL
scala> val string1c = confDemo3.getString( "string1 ")
string1c: String = Hello from demo2.properties
scala>
val int1c = confDemo3.getInt( "int1 ") int1c: Int = 13
scala>
val double1c = confDemo3.getDouble( "double1 ") double1c: Double = 123.45
scala>
val double2b = confDemo3.getDouble( "double2 ") double2b: Double = 999.99

We can also access key/value pairs from the JSON file.

Scala REPL
scala> val firstName = confDemo3.getString( "firstName ")
firstName: String = Jane
scala>
val isAlive = confDemo3.getString( "isAlive ") isAlive: String = true
scala>
val height_cm = confDemo3.getString( "height_cm ") height_cm: String = 147.64

JSON is a more flexible format than Java properties files because it can store hierarchical data. Config can access the hierarchy passing a path to a Config object. Here we see the paths address.streetAddress and address.city used to extract the value of the streetAddress and city keys from the address JSON object.

Scala REPL
scala> val streetAddress = confDemo3.getString( "address.streetAddress ")
streetAddress: String = 21 2nd Street
scala>
val city = confDemo3.getString( "address.city ") city: String = New York

JSON supports lists and objects, which are not possible with Java properties files. Config can handle these types easily. Config.getList can parse a JSON list, and returns a ConfigList, and key/value pairs in a ConfigList can be parsed using ConfigList.atKey.

Scala REPL
scala> val phoneNumbers: ConfigList = confDemo3.getList( "phoneNumbers ")
phoneNumbers: com.typesafe.config.ConfigList = SimpleConfigList([{ "number ": "212 555-1234 ", "type ": "home "},{ "number ": "646 555-4567 ", "type ": "office "}])
scala>
val phoneType = phoneNumbers.atKey( "type ") phoneType: com.typesafe.config.Config = Config(SimpleConfigObject({ "type ":[{ "number ": "212 555-1234 ", "type ": "home "},{ "number ": "646 555-4567 ", "type ": "office "}]}))
scala>
val phoneNumber = phoneNumbers.atKey( "number ") phoneNumber: com.typesafe.config.Config = Config(SimpleConfigObject({ "number ":[{ "number ": "212 555-1234 ", "type ": "home "},{ "number ": "646 555-4567 ", "type ": "office "}]}))

Config.getObject can parse a JSON object, and returns a ConfigObject. ConfigObject.toConfig converts the ConfigObject to a Config object, from which key/value pairs can be extracted as previously shown.

Scala REPL
scala> val addresses: Config = confDemo3.getObject( "address ").toConfig
addresses: com.typesafe.config.Config = Config({ "city ": "New York ", "postalCode ": "10021-3100 ", "state ": "NY ", "streetAddress ": "21 2nd Street "})
scala>
val streetAddress2 = addresses.getString( "streetAddress ") streetAddress2: String = 21 2nd Street
scala>
val city2 = addresses.getString( "city ") city2: String = New York

Config does a pretty good job of parsing JSON, but because it is Java-based it cannot convert JSON objects into Scala case classes. If you want to do that, you should consider more capable JSON parsers for Scala such as circe, JSON4S and others.

Generating JSON

Lets convert the configuration data stored in demo.properties into JSON. First I’ll load just that file into a new Config instance.

Scala REPL
scala> import com.typesafe.config._
import com.typesafe.config._
scala>
val confDemo: Config = ConfigFactory.parseResources( "demo.properties ") confDemo: com.typesafe.config.Config = Config(SimpleConfigObject({ "bytes1 ": "2K ", "bytes2 ": "3G ", "double1 ": "123.45 ", "elapsedTime ": "1 day ", "int1 ": "42 ", "string1 ": "Hello from demo.properties "}))

ConfigValue is a Java interface implemented by ConfigList and ConfigObject.. A ConfigValue instance can be obtained from a suitable Config object’s root property as follows:

Scala REPL
scala> confDemo.root
res0: com.typesafe.config.ConfigObject = SimpleConfigObject({ "bytes1 ": "2K ", "bytes2 ": "3G ", "double1 ": "123.45 ", "elapsedTime ": "1 day ", "int1 ": "42 ", "string1 ": "Hello from demo.properties "}) 

ConfigValue instances can be converted into JSON by invoking their render method. Before we do that, however, we need to specify the correct ConfigRenderOptions so the ConfigValue.render method generates the desired output. Those options can be set by chaining method calls.

Typical options for generating JSON are concise (which removes whitespace and comments), setJson(true) and setFormatted(true). In this example, I’m storing the options in a variable called options.

Scala REPL
scala> val options = ConfigRenderOptions.concise
                                        .setJson(true)
                                        .setFormatted(true)
options: com.typesafe.config.ConfigRenderOptions = ConfigRenderOptions(formatted,json) 

We can generate formatted JSON from the configuration object:

Scala REPL
scala> confDemo.root
               .render(options)
res1: String =
 "{
     "bytes1 " :  "2K ",
     "bytes2 " :  "3G ",
     "double1 " :  "123.45 ",
     "elapsedTime " :  "1 day ",
     "int1 " :  "42 ",
     "string1 " :  "Hello from demo.properties "
}
 " 

Unformatted JSON can be generated by calling setFormatted(false).

Scala REPL
scala> val options = ConfigRenderOptions.concise.setJson(true).setFormatted(false)
options: com.typesafe.config.ConfigRenderOptions = ConfigRenderOptions(json)
scala>
confDemo.root.render(options) res2: String = { "bytes1 ": "2K ", "bytes2 ": "3G ", "double1 ": "123.45 ", "elapsedTime ": "1 day ", "int1 ": "42 ", "string1 ": "Hello from demo.properties "}

Working with HOCON Files

The HOCON format is just an incremental improvement on JSON. It is also the standard configuration file format for Akka and Play. The HOCON syntax is more flexible and included files are supported.

So far we have used Config.parseResources to load the contents of a configuration file from the classpath. The Config.load method works just the same, and also provides default values for standard Akka and Play configuration parameters. By default, Config.load looks for a file called application.conf on the classpath.

The courseNotes project contains a file called courseNotes/src/main/resources/application.conf, which looks like this:

application.conf
include  "custom.conf "
aws { accessKey = "applicationAccessKey " accessKey = ${?ACCESS_KEY}
secretKey = "applicationSecretKey " secretKey = ${?SECRET_KEY} }

courseNotes/src/main/resources/custom.conf is included by application.conf, and it looks like this:

custom.conf
keyName1 :  "Value 1 "
keyName2 = true
nested.key.name3 : 5
nested {
  key {
    name4 = {
       "one " : 10,
       "two " : 12
    }
  }
}
blarg = "default blarg " blarg = ${?BLARG}

Given the above configuration files, we could read the values they contain as follows.

Shell
$ sbt console
Loading /usr/share/sbt/bin/sbt-launch-lib.bash
[info] Loading global plugins from /home/mslinn/.sbt/0.13/plugins
[info] Loading project definition from /home/mslinn/work/course_scala_intermediate_code/courseNotes/project
[info] Set current project to IntermediateScalaCourse (in build file:/home/mslinn/work/course_scala_intermediate_code/courseNotes/)
import java.io.File
import java.net.URL
import scala.sys.process._
Welcome to Scala version 2.11.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_60).
Type in expressions to have them evaluated.
Type :help for more information.
scala>
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
scala>
val conf = ConfigFactory.load ...

Notice that ConfigFactory.load pulls in a lot more information than was provided in the configuration file; the extra information is the default Akka configuration data I just mentioned. If you are not working with Akka or Play Framework, then you probably should use ConfigFactory.parseResources as shown above instead.

Now we can extract strongly typed values from the configuration as before.

Scala REPL
scala> val value1 = conf.getString( "keyName1 ")
value1: String = Value 1
scala>
val value2 = conf.getBoolean( "keyName2 ") value2: Boolean = true

Again, we can specify the value of an inner object property using dotted syntax.

Scala REPL
scala> val value3 = conf.getInt( "nested.key.name3 ")
value3: Int = 5 

We can also obtain a subset of the configuration data using getConfig, and then refer to inner properties without having to qualify them using dotted syntax.

Scala REPL
scala> val confNested = conf.getConfig( "nested.key.name4 ")
confNested: com.typesafe.config.Config = Config(SimpleConfigObject({ "one ":10, "two ":12}))
scala>
val value5 = confNested.getInt( "one ") value5: Int = 10
scala>
val value6 = confNested.getInt( "two ") value6: Int = 12

Creating Config Objects from Strings

ConfigFactory.parseString can create a Config object by reading configuration values from a String. This is useful for specifying defaults and for testing. The contents of custom.conf could be expressed as a String this way.

Scala REPL
scala> val string =  " " "
  keyName1 :  "Value 1 "
  keyName2 = true
  nested.key.name3 : 5
  nested { key { name4 = {  "one " : 10,  "two " : 12 } } }
  array : [ 1, 2, 3]
   " " "
string: String =
 "
keyName1 :  "Value 1 "
keyName2 = true
nested.key.name3 : 5
nested { key { name4 = {  "one " : 10,  "two " : 12 } } }
array : [ 1, 2, 3]
 " 

The values can be extracted from the resulting Config object just as if they had originated from a .conf file.

Scala REPL
scala> val conf2 = ConfigFactory.parseString(string)
conf2: com.typesafe.config.Config = Config(SimpleConfigObject({ "array ":[1,2,3], "keyName1 ": "Value 1 ", "keyName2 ":true, "nested ":{ "key ":{ "name3 ":5, "name4 ":{ "one ":10, "two ":12}}}}))
scala>
val value1b = conf2.getString( "keyName1 ") value1b: String = Value 1
scala>
val value2b = conf2.getBoolean( "keyName2 ") value2b: Boolean = true
scala>
val value3b = conf2.getInt( "nested.key.name3 ") value3b: Int = 5
scala>
val confNested2 = conf2.getConfig( "nested.key.name4 ") confNested2: com.typesafe.config.Config = Config(SimpleConfigObject({ "one ":10, "two ":12}))
scala>
val value5b = confNested2.getInt( "one ") value5b: Int = 10
scala>
val value6b = confNested2.getInt( "two ") value6b: Int = 12
scala>
val array = conf2.getIntList( "array ") array: java.util.List[Integer] = [1, 2, 3]

Arrays of Properties

There is a special syntax for concatenating arrays in a .conf file. This example is taken from the Cadenza Play Framework webapp’s unit testing setup (Cadenza powered the original ScalaCourses webapp). In that unit test setup, a .config file sets an array of values called play.modules.enabled, then includes another .config file. Here is an excerpt from the first .config file.

.conf file
play {
  modules {
    enabled = ${?play.modules.enabled} [
       "modules.cadenza.CadenzaAuthViewsModule ",
       "modules.cadenza.CadenzaModule "
    ]
    disabled = []
  }
}
include  "silhouette.application.conf "

Notice that the assignments to play.modules.enabled starts with the incantation ${?play.modules.enabled}. This concatenates the previous value of play.modules.enabled with the value in square brackets. Because there was no previous definition for play.modules.enabled, just the value provided in square brackets is assigned to play.modules.enabled.

The included .config file (silhouette.application.conf) looks like this:

.conf file
play {
  modules {
    enabled = ${?play.modules.enabled} [
       "com.mohiva.play.silhouette.api.actions.SecuredActionModule ",
       "com.mohiva.play.silhouette.api.actions.UnsecuredActionModule ",
       "com.mohiva.play.silhouette.api.actions.UserAwareActionModule ",
       "modules.silhouette.BaseModule ",
       "modules.silhouette.SilhouetteModule ",
       "play.api.libs.openid.OpenIDModule ",
       "play.api.libs.ws.ahc.AhcWSModule ",
       "play.api.libs.mailer.MailerModule ",
       "play.api.libs.mailer.SMTPConfigurationModule ",
       "play.filters.csrf.CSRFModule "
    ]
  }
}

Now that play.modules.enabled has a value, that value is concatenated with the value within square brackets. Notice that the reference to the previous value had to be fully qualified: ${?play.modules.enabled}; merely specifying ${?enabled} does not work. The resulting value for the play.modules object becomes.

play {
  modules {
    enabled = [
       "modules.cadenza.CadenzaAuthViewsModule ",
       "modules.cadenza.CadenzaModule ",
       "com.mohiva.play.silhouette.api.actions.SecuredActionModule ",
       "com.mohiva.play.silhouette.api.actions.UnsecuredActionModule ",
       "com.mohiva.play.silhouette.api.actions.UserAwareActionModule ",
       "modules.silhouette.BaseModule ",
       "modules.silhouette.SilhouetteModule ",
       "play.api.libs.openid.OpenIDModule ",
       "play.api.libs.ws.ahc.AhcWSModule ",
       "play.api.libs.mailer.MailerModule ",
       "play.api.libs.mailer.SMTPConfigurationModule ",
       "play.filters.csrf.CSRFModule "
    ]
    disabled = []
  }
}

Overriding with Environment and Java System Variables

HOCON files can allow for overrides from environment variables so sensitive data can be provided at runtime instead being committed to a code base. The syntax is peculiar, but works quite well. The file src/main/resources/override.conf looks like this.

HOCON
blarg =  "default blarg "
blarg = ${?BLARG}

The value of blarg will depend on whether an environment variable or Java System variable called BLARG is defined. If not, the value default blarg will be used. Before you can access the values from your code you must call resolve.

Scala REPL
scala> sbt console
...usual output not shown ...
scala> import com.typesafe.config.{ConfigObject, ConfigList, ConfigFactory}
import com.typesafe.config.{ConfigObject, ConfigList, ConfigFactory}
scala>
val confOverride = ConfigFactory.parseResources( "override.conf ").resolve confOverride: com.typesafe.config.Config = Config(SimpleConfigObject({ "blarg ": "default blarg "}))
scala>
val blarg = confOverride.getString( "blarg ") blarg: String = default blarg

Now lets set some environment variables and restart sbt console.

Scala REPL
scala> export BLARG= "blarg set from environment variable "
scala> sbt console
...usual output not shown...
scala> import com.typesafe.config.{ConfigObject, ConfigList, ConfigFactory}
import com.typesafe.config.{ConfigObject, ConfigList, ConfigFactory}
scala>
val confOverride = ConfigFactory.parseResources( "override.conf ").resolve confOverride: com.typesafe.config.Config = Config(SimpleConfigObject({ "blarg ": "blarg set from environment variable ", "item ": "Item set from environment variable "}))
scala>
val blarg = confOverride.getString( "blarg ") blarg: String = blarg set from environment variable

The Config documentation also states that Java system variables can be used to pass configuration data. This is true, however if your program uses SBT or Play to launch you will likely have a very difficult time. I recommend you avoid doing this.

Scala Compatibility

Because Config was written in Java, the collections it provides don’t have the handy Scala behavior. As we learned in the To Converters lecture, you can enhance the behavior by importing JavaConverters and using the asScala method.

Scala REPL
scala> import com.typesafe.config.{ConfigObject, ConfigList, ConfigFactory}
import com.typesafe.config.{ConfigObject, ConfigList, ConfigFactory}
scala>
import collection.JavaConverters._ import collection.JavaConverters._
scala>
val conf2 = ConfigFactory.parseResources( "custom.conf ") conf2: com.typesafe.config.Config = Config(SimpleConfigObject({ "keyName1 ": "Value 1 ", "keyName2 ":true, "nested ":{ "key ":{ "name3 ":5, "name4 ":{ "one ":10, "two ":12}}}}))
scala>
val keys = conf2.entrySet.asScala.map(_.getKey) keys: scala.collection.mutable.Set[String] = Set(array, nested.key.name4.one, keyName1, keyName2, nested.key.name4.two, nested.key.name3)
scala>
val keyName1 = conf2.getString( "keyName1 ") keyName1: String = Value 1
scala>
val confNested3 = conf2.getConfig( "nested ") configNested3: com.typesafe.config.Config = Config(SimpleConfigObject({ "key ":{ "name3 ":5, "name4 ":{ "two ":12, "one ":10}}}))
scala>
confNested3.entrySet.asScala.map(_.getKey) res9: scala.collection.mutable.Set[String] = Set(key.name4.two, key.name4.one, key.name3)
scala>
confNested3.getString( "key.name3 ") res10: String = 5

PureConfig

Several Scala wrappers for Config exist, including PureConfig, ScalaConfig, StaticConfig, ValidatedConfig, CediConfig and kxbmap. I like PureConfig best, and we’ll discuss a short demo program now.

The PureConfig documentation is terse, so I’ll try to give more information. There is also some useful documentation hidden in the docs directory of the project on GitHub.

The dependency is declared in build.sbt like this (we discussed how to declare dependencies in the SBT Project Setup lecture of the Introduction to Scala course).

build.sbt fragment
"com.github.pureconfig" %%  "pureconfig" %  "0.7.2" withSources()

PureConfig requires that a case class be declared that matches the content of the HOCON configuration file. This allows the case class to automatically be populated by the information from the configuration. I like this idea, because it ends up simplifying your code and makes it much easier to maintain. This also means you don’t need to write a lot of low-level code that calls getXXX methods to read values from the underlying Config object. What’s more, PureConfig has an internal cache, so it can be repeatedly queried efficiently. The cache is cleared before each call to pureconfig.load and pureconfig.loadOrThrow.

PureConfig also works best if the case class is strongly typed; in other words, instead of declaring the properties to be String and Int, etc, define highly specific types. Use value classes whenever possible, because they can provide type safety without runtime overhead. Value classes were discussed in the Implicit Values lecture. Each value class will need an implicit converter, and they are simple to write.

More recent versions of PureConfig don’t work well with value classes, so some experimentation might be required to see what works.

PureConfig works well with properties that themselves are case classes. This lets you easily define a complex structure and populate it from a HOCON configuration file.

Here is an example of a HOCON file, provided as courseNotes/src/main/resources/pure.conf.

pure.conf
ew {
  console {
    enabled = true
    enabled = ${?EW_CONSOLE_ENABLED}
  }
feed { port = 9090 port = ${?EW_FEED_PORT} }
repl { home = "~ " home = ${?EW_REPL_HOME} }
speciesDefaults { attributeMinimum = 0 attributeMinimum = ${?EW_ATTRIBUTE_MINIMUM}
attributeMaximum = 100 attributeMaximum = ${?EW_ATTRIBUTE_MAXIMUM}
eventQLength = 20 eventQLength = ${?EW_EVENT_QUEUE_LENGTH}
historyLength = 20 historyLength = ${?EW_HISTORY_LENGTH} }
sshServer { address = localhost address = ${?EW_SSH_SERVER_ADDRESS}
ammoniteHome = ~ ammoniteHome = ${?EW_SSH_SERVER_HOME}
enabled = true enabled = ${?EW_SSH_SERVER_ENABLED}
hostKeyFile = ${?EW_SSH_SERVER_HOST_KEY_FILE}
password = " " password = ${?EW_SSH_SERVER_PASSWORD}
port = 1101 port = ${?EW_SSH_SERVER_PORT}
userName = "repl " userName = ${?EW_SSH_SERVER_USER_NAME} } }

The case class that will be populated by pure.config is courseNotes/src/main/scala/PureConfigFun.scala Notice that all of its properties are strongly typed, and that default values are provided.

PureConfigFun.scala fragment 1
import java.nio.file.{Path, Paths}
import PureConfigFun._
case class PureConfigFun( console: ConsoleConfig = defaultConsoleConfig, feed: FeedConfig = defaultFeedConfig, repl: ReplConfig = defaultReplConfig, speciesDefaults: SpeciesDefaults = defaultSpeciesConfig, sshServer: SshServer = defaultSshServerConfig )

The companion object has several moving parts, which we’ll look at individually. The general outline is.

PureConfigFun.scala fragment 2
object PureConfigFun {
  import pureconfig.{CamelCase, ConfigConvert, ConfigFieldMapping, ProductHint}
  import pureconfig.error.{CannotConvert, ConfigReaderFailures, ConfigValueLocation}
  import pureconfig.ConfigConvert._
  import com.typesafe.config.{ConfigValue, ConfigValueFactory, ConfigValueType}
val defaultConsoleConfig = ConsoleConfig() val defaultFeedConfig = FeedConfig() val defaultReplConfig = ReplConfig() val defaultSpeciesConfig = SpeciesDefaults() val defaultSshServerConfig = SshServer()
// TODO add implicits that control how PureConfig works here
lazy val confPath: Path = new java.io.File(getClass.getClassLoader.getResource( "pure.conf ").getPath).toPath
def load: Either[ConfigReaderFailures, PureConfigFun] = pureconfig.loadConfig[PureConfigFun](confPath, "ew ")
def loadOrThrow: PureConfigFun = pureconfig.loadConfigOrThrow[PureConfigFun](confPath, "ew ")
def apply: PureConfigFun = loadOrThrow }

Notice that the default values are defined in the companion object, and the load method uses the file called pure.conf (which should be in the classpath, normally by placing it in the src/main/resources/ directory). The load method as written only looks at the ew key’s value, which PureConf calls the ew namespace. I also defined a method called loadOrThrow which either loads the contents of pure.conf, or throws a ConfigReaderException if any problem was enountered while parsing the configuration file.

If you want to parse the entire configuration file, don’t supply a namespace when invoking pureconfig.loadConfig, like this:

PureConfigFun.scala fragment 3
def load: Either[ConfigReaderFailures, PureConfigFun] = pureconfig.loadConfig[PureConfigFun](confPath)
def loadOrThrow: PureConfigFun = pureconfig.loadConfigOrThrow[PureConfigFun](confPath)

A Closer Look

The typesafe ConfigFactory.load method loads the given file but also adds overrides and fallbacks, including values taken from the system environment variables. If you don’t give provide namespace to the pureconfig.loadConfig methods, PureConfig will return an unknown key failures for every system property not specified as a property in your case class.

To debug this, load a raw ConfigValue to see what’s going on and then invoke render so you can see from where each configuration value was loaded from.

Scala REPL
scala> pureconfig.loadConfigOrThrow[ConfigValue](confPath)
                 .render
                 .lines foreach println
[...]
    "user " : {
        # system properties
        "country " :  "US ",
        # system properties
        "dir " :  "/home/foo/prog/pure-config-test ",
        # system properties
        "home " :  "/home/foo ",
        # system properties
        "language " :  "en ",
        # system properties
        "name " :  "foo ",
        # system properties
        "timezone " :  "Europe/Bar "
  }
[...] 

The solution to this problem is to either specify a ProductHint with the value allowUnknownKeys = true or to always use a namespace that you know won’t contain anything else except the configuration you need.

Here are the classes that will automatically be instantiated and populated when the configuration file is read. I defined as many of them as possible as value classes.

Scala code
case class FeedConfig(port: Port = Port(1100))
case class ConsoleConfig(enabled: Boolean = true) extends AnyVal
case class Port(value: Int) extends AnyVal
case class ReplConfig( home: Path = Paths.get(System.getProperty( "user.home ")) )
case class SpeciesDefaults( attributeMinimum: Int = 0, attributeMaximum: Int = 100, eventQLength: Int = 25, historyLength: Int = 20 )
case class SshServer( address: String = "localhost ", ammoniteHome: Path = Paths.get(System.getProperty( "user.home ") + "/.ammonite "), enabled: Boolean = true, hostKeyFile: Option[Path] = None, //Some(Paths.get(System.getProperty( "user.home ") + "/.ssh/id_rsa ")), password: String = " ", port: Port = Port(1101), userName: String = "repl " )

We need a way to convert an Int into a Port when a configuration file is read. PureConfig’s ConfigConvert type can be extended to do that. Define the subclass before the configuration is loaded so the implicit conversion is in scope.

Scala code
implicit val readPort = new ConfigConvert[Port] {
  override def from(config: ConfigValue): Either[ConfigReaderFailures, Port] = {
    config.valueType match {
      case ConfigValueType.NUMBER =>
        Right(Port(config.unwrapped.asInstanceOf[Int]))
case _ => fail(CannotConvert(config.render, "Port ", s "A port should be a number, but ${ config.valueType } was found ", ConfigValueLocation(config))) } }
override def to(port: Port): ConfigValue = ConfigValueFactory.fromAnyRef(port.value) }

It would be nice to allow tilde (~) to be used to specify the user’s home directory. To do this, we need to define special handling for java.nio.file.Path conversions, like this.

Scala code
val expandTilde: String => Path =
  (string: String) => Paths.get(string.replace( "~ ", sys.props( "user.home ")))
import pureconfig.ConvertHelpers._ implicit val overridePathReader: ConfigReader[Path] = ConfigReader.fromString[Path](catchReadError(expandTilde))

It would be useful to have PureConfig throw an error if an unknown key is found. To do that, define an implicit ProductHint, before PureConfig loads the configuration.

Scala code
implicit val hint: ProductHint[PureConfigFun] = ProductHint[PureConfigFun](
  allowUnknownKeys = false
)

The default fieldMapping is KebabCase. Because the keys in the configuration file are CamelCase, we need to add a fieldMapping hint to the implicit ProductHint.

Scala code
implicit val hint: ProductHint[PureConfigFun] = ProductHint[PureConfigFun](
  allowUnknownKeys = false,
  fieldMapping = ConfigFieldMapping(CamelCase, CamelCase)
)

Now for a short test application.

Scala code
object PureConfigTest extends App {
  val pureConfigFun = PureConfigFun.load
  println(pureConfigFun)
}

You can run it by typing:

Shell
$ sbt  "runMain PureConfigTest"
Lots of output...
Right(PureConfigFun(ConsoleConfig(true),FeedConfig(Port(9090)),ReplConfig(~),SpeciesDefaults(0,100,25,20),SshServer(localhost,C:\Users\mslin_000\.ammonite,true,None,,Port(1101),repl))) 

I’m not comfortable with this program. If the config file is missing, and that should be considered as an error, the program silently uses the default value. Instead, I prefer an exception to be raised. This can be done by calling pureconfig.loadConfigOrThrow instead of pureconfig.load.

Scala code
object PureConfigTest2 extends App {
  val pureConfigFun = PureConfigFun.loadOrThrow
  println(pureConfigFun)
}

You can run this program by typing.

Shell
$ sbt  "runMain PureConfigTest2"
Lots of output...
PureConfigFun(ConsoleConfig(true),FeedConfig(Port(9090)),ReplConfig(~),SpeciesDefaults(0,100,25,20),SshServer(localhost,C:\Users\mslin_000\.ammonite,true,None,,Port(1101),repl))

* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.