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.
Top comments (3)
Sir, I admit I may be slow, but I created a file (.toml) and placed it in /tmp/configfile.toml. I used the example code to do a loadConfigorThrow per your example.
The code compiles, and attempts to run, but, it informs me it can't open /tmp/configfile.toml.
OK, I see the file there, I see it is 0644 so anyone can read it. How can I tell WHY Hoplite can't open it or read it?
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.