loading...

Boilerplate free config in Kotlin using Hoplite

sksamuel profile image Stephen Samuel ・3 min read

TL;DR: Loading Yaml, Json, etc config files into Kotlin data classes without boilerplate and with error checking with Hoplite.

Hello Kotliners!

For those of us who like to develop in a more functional way and avoid using Spring, we find ourselves looking for a way to handle configuration in our apps.

The most basic way is to use a java Properties file and use key=value pairs, or we may choose to use the popular Lightbend Config library and write config in their Json superset called Hocon. Whatever we choose we end up writing code like the following:

val jdbcUrl = config.getString("jdbc.url")

In other words, pulling the value out of the config by referencing the key. The config values may be extracted when they are needed at the call point, or we may choose to place all the config values into some central object and pass that about. Either way, there's no guarantee the key we reference actually exists until the config is required.

Even worse, if the value inside the config is not compatible with the target type, we may run into a conversion problem. We've all been bitten by a typo where we had :8080 as our port number only to find out 10 minutes into deployment we get a number format exception.

Some libraries may support marshalling automatically to some basic types, like doubles and bools, but often go no further than the basic primitives and collections.

Using Data Classes

Rather than pulling config out by key, we should push config into classes.

We start by declaring a data class (including nested classes).

data class Database(val host: String, val port: Int, val database: String)
data class WebServer(val resources: Path, val port: Int, val timeout: Duration) 
data class Config(val env: String, val db: Database, val server: WebServer)

Next we write our config, in either Yaml, Json, Toml, Hocon, or Java Props. I'll use Yaml here, in a file called staging.yaml

env: staging

db:
  host: staging.wibble.com
  port: 3306
  database: userprofiles

server:
  port: 8080
  resources: /var/www/web
  timeout: 10s

Finally, we use the ConfigLoader class to marshall the config directly into our config classes.

val cfg = ConfigLoader().loadConfigOrThrow<Config>("/staging.yaml")

Now the cfg instance is a fully inflated version of the configuration file, with all values converted into the defined types.

Note: The files should be on the classpath. We can load from files outside the classpath instead if we wish by using instances of Path.

Easy Errors

As we mentioned at the start, one of the biggest problems with config is when things go wrong. With Hoplite, errors are beautifully displayed as soon as the config is marshalled.

Let's use the same config as before, but this time with a file with a bunch of errors:

envvv: staging

db:
  host: staging.wibble.com
  port: .3306
  databas: userprofiles

server:
  port: localhost
  resources: /var/www/web
  timeout: 10ty

Using this file, gives the following error output:

Exception in thread "main": Error loading config because:

    - Could not instantiate 'com.example.Config' because:

        - 'env': Missing from config

        - 'db': - Could not instantiate 'com.example.Database' because:

            - 'host': Missing from config

            - 'port': Could not decode .3306 into a Int (/foo.yml:4:8)

            - 'database': Missing from config

        - 'server': - Could not instantiate 'com.example.WebServer' because:

            - 'port': Could not decode localhost into a Int (/foo.yml:8:8)

            - 'timeout': Required type java.time.Duration could not be decoded from a String value: 10ty (/foo.yml:10:11)

You can see how easy it is to debug this file - detailed error messages showing the exact key, plus where possible the line number and file name is included. The errors include values that could not be converted to a number, missing values, and erroneous formats for a Duration.

Supported Types

Hoplite supports many different types out of the box - batteries included as the cool kids say. These are your usual primitive types, collections, enums, dates, durations, BigDecimal, BigInteger, UUID, files, paths and so on. In addition the data types from Arrow - NonEmptyList, Tuples and Option are supported as well.

It's also very easy to add support for custom types if you wish. Just implement the Decoder interface and add this via a service loader.

There's plenty more to Hoplite, so I'll point you to the official docs for further reading.

Posted on by:

sksamuel profile

Stephen Samuel

@sksamuel

Kotlin and Scala programmer based in Chicago

Discussion

pic
Editor guide
 

Amazing work! Are you working internally with LightBend config?

 

The hoplite-hocon library wraps lightbend yes. The others wrap different libraries like jackson or snake yaml.