Hoplite
A boilerplate-free Kotlin config library for loading configuration files as data classes
Install / Use
/learn @sksamuel/HopliteREADME
Hoplite <img src="logo.png" height=40>
Hoplite is a Kotlin library for loading configuration files into typesafe classes in a boilerplate-free way. Define your config using Kotlin data classes, and at startup Hoplite will read from one or more config files, mapping the values in those files into your config classes. Any missing values, or values that cannot be converted into the required type will cause the config to fail with detailed error messages.
<img src="https://img.shields.io/maven-central/v/com.sksamuel.hoplite/hoplite-core.svg?label=latest%20release"/>
<img src="https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fcentral.sonatype.com%2Frepository%2Fmaven-snapshots%2Fcom%2Fsksamuel%2Fhoplite%2Fhoplite-core%2Fmaven-metadata.xml&strategy=highestVersion&label=maven-snapshot">
Features
- Multiple formats: Write your configuration in several formats: Yaml, JSON, Toml, Hocon, or Java .props files or even mix and match formats in the same system.
- Property Sources: Per-system overrides are possible from JVM system properties, environment variables, JNDI or a per-user local config file.
- Batteries included: Support for many standard types such as primitives, enums, dates, collection
types, inline classes, uuids, nullable types, as well as popular Kotlin third party library types such
as
NonEmptyList,OptionandTupleXfrom Arrow. - Custom Data Types: The
Decoderinterface makes it easy to add support for your custom domain types or standard library types not covered out of the box. - Cascading: Config files can be stacked. Start with a default file and then layer new configurations on top. When resolving config, lookup of values falls through to the first file that contains a definition. Can be used to have a default config file and then an environment specific file.
- Beautiful errors: Fail fast at runtime, with beautiful errors showing exactly what went wrong and where.
- Preprocessors: Support for several preprocessors that will replace placeholders with values resolved from external configs, such as AWS Secrets Manager, Azure KeyVault and so on.
- Reloadable config: Trigger config reloads on a fixed interval or in response to external events such as consul value changes.
- Prefix Binding: Optionally, load configuration sources once, and then bind individual prefix paths into independent config types.
Changelog
See the list of changes in each release here.
Getting Started
Add Hoplite to your build:
implementation 'com.sksamuel.hoplite:hoplite-core:<version>'
You will also need to include a module for the format(s) you to use.
Next define the data classes that are going to contain the config. You should create a top level class which can be named simply Config, or ProjectNameConfig. This class then defines a field for each config value you need. It can include nested data classes for grouping together related configs.
For example, if we had a project that needed database config, config for an embedded HTTP server, and a field which contained which environment we were running in (staging, QA, production etc), then we may define our classes like this:
data class Database(val host: String, val port: Int, val user: String, val pass: String)
data class Server(val port: Int, val redirectUrl: String)
data class Config(val env: String, val database: Database, val server: Server)
For our staging environment, we may create a YAML (or Json, etc) file called application-staging.yaml.
The name doesn't matter, you can use any convention you wish.
env: staging
database:
host: staging.wibble.com
port: 3306
user: theboss
pass: 0123abcd
server:
port: 8080
redirectUrl: /404.html
Finally, to build an instance of Config from this file, and assuming the config file was on the classpath, we can simply execute:
val config = ConfigLoaderBuilder.default()
.addResourceSource("/application-staging.yml")
.build()
.loadConfigOrThrow<Config>()
If the values in the config file are compatible, then an instance of Config will be returned.
Otherwise, an exception will be thrown containing details of the errors.
Config Loader
As you have seen from the getting started guide, ConfigLoader is the entry point to using Hoplite. We create an
instance of this loader class through the ConfigLoaderBuilder builder. To this builder we add sources, configuration,
enable reports, add preprocessors and more.
To create a default builder, use ConfigLoaderBuilder.default() and after adding your sources, call build.
Here is an example:
ConfigLoaderBuilder.default()
.addResourceSource("/application-prod.yml")
.addResourceSource("/reference.json")
.build()
.loadConfigOrThrow<MyConfig>()
The default method on ConfigLoaderBuilder sets up recommended defaults. If you wish to start with a completely empty
config builder, then use ConfigLoaderBuilder.empty().
There are two ways to retrieve a populated data class from config. The first is to throw an exception if the config
could not be resolved. We do this via the loadConfigOrThrow<T> function. Another is to return a ConfigResult validation
monad via the loadConfig<T> function if you want to handle errors manually.
For most cases, when you are resolving config at application startup, the exception based approach is better. This is because you typically want any errors in config to abort application bootstrapping, dumping errors immediately to the console.
Prefix Binding
Prefixes can be used to bind selected config to independent data classes. This is useful for modular config loading. For example, independent modules or plugins load their own config from a common set of configuration sources.
For example a yaml source containing
module1:
foo: bar
module2:
baz: qux
can be bound to:
data class Module1Config(val foo: String)
data class Module2Config(val baz: String)
The best way to do this is to obtain a ConfigBinder from ConfigLoader, for example:
val configBinder = ConfigLoaderBuilder.default()
.addResourceSource("/application-prod.yml")
.addResourceSource("/reference.json")
.build()
.configBinder()
// generally a ConfigBinder will be provided via DI, and these calls will be in their own modules!
val module1Config = configBinder.bindOrThrow<Module1Config>("module1")
val module2Config = configBinder.bindOrThrow<Module2Config>("module2")
With this approach, the configuration sources will only be read and parsed a single time, but can be bound to independent data classes as many times as is necessary.
A prefix can also be provided directly to loadConfig and its variants if only one prefix needs to be loaded.
The prefix value does not have to refer only to root properties -- a prefix of foo.bar will access config at the foo.bar node in the config tree that ConfigLoader creates.
Beautiful Errors
When an error does occur, if you choose to throw an exception, the errors will be formatted in a human-readable way
along with as much location information as possible. No more trying to track down a NumberFormatException in a 400
line config file.
Here is an example of the error formatting for a test file used by the unit tests. Notice that the errors indicate which file the value was pulled from.
Error loading config because:
- Could not instantiate 'com.sksamuel.hoplite.json.Foo' because:
- 'bar': Required type Boolean could not be decoded from a Long (classpath:/error1.json:2:19)
- 'baz': Missing from config
- 'hostname': Type defined as not-null but null was loaded from config (classpath:/error1.json:6:18)
- 'season': Required a value for the Enum type com.sksamuel.hoplite.json.Season but given value was Fun (/home/user/default.json:8:18)
- 'users': Defined as a List but a Boolean cannot be converted to a collection (classpath:/error1.json:3:19)
- 'interval': Required type java.time.Duration could not be decoded from a String (classpath:/error1.json:7:26)
- 'nested': - Could not instantiate 'com.sksamuel.hoplite.json.Wibble' because:
- 'a': Required type java.time.LocalDateTime could not be decoded from a String (classpath:/error1.json:10:17)
- 'b': Unable to locate a decoder for java.time.LocalTime
Supported Formats
Hoplite supports config files in several formats. You can mix and match formats if you really want to. For each format you wish to use, you must include the appropriate hoplite module on your classpath. The format that hoplite uses to parse a file is determined by the file extension.
| Format | Module | File Extensions |
|:--------------------------------------------------------------------|:-------------------------------------------------------------------|:--------------------|
| Json | hoplite-json | .json |
| Yaml Note: Yaml files are limited 3mb in size. | hoplite-yaml | .yml, .yaml |
| Toml
