SkillAgentSearch skills...

Hoplite

A boilerplate-free Kotlin config library for loading configuration files as data classes

Install / Use

/learn @sksamuel/Hoplite
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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.

master <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, Option and TupleX from Arrow.
  • Custom Data Types: The Decoder interface 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

View on GitHub
GitHub Stars1.0k
CategoryDevelopment
Updated11h ago
Forks89

Languages

Kotlin

Security Score

100/100

Audited on Mar 31, 2026

No findings