Get started / Kotlin
docuconf for Kotlin
You keep writing a Hoplite data class. docuconf adds annotations for descriptions, bounds, URL schemes and file inputs, checks every variable and file before Hoplite binds the class, and exports the contract.
- Builds on
- Hoplite 3
- Package
- dev.docuconf:docuconf-hoplite (Maven)
- Requires
- JDK 17, Kotlin 2.2
- Status
- v0.1 alpha, not yet released · conformance 134 of 134
- Source
- docuconf-kotlin · the orders example
Not published yet
main branch. After the first release it will be implementation("dev.docuconf:docuconf-hoplite:0.1.0") from Maven Central.The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.
1.Install
The artifacts are not on Maven Central yet. Clone the repository next to your project and include it, and its dev.docuconf Gradle plugin, as a Gradle composite build (JDK 17+); Gradle builds both from source:
git clone https://github.com/Docuconf/docuconf-kotlin ../docuconf-kotlinpluginManagement {
includeBuild("../docuconf-kotlin/docuconf-gradle-plugin")
repositories {
gradlePluginPortal()
mavenCentral()
}
}
includeBuild("../docuconf-kotlin")plugins {
kotlin("jvm") version "2.2.21"
application
id("dev.docuconf")
}
dependencies {
implementation("dev.docuconf:docuconf-hoplite:0.1.0-SNAPSHOT")
testImplementation(kotlin("test"))
}
kotlin {
jvmToolchain(17)
}
application {
mainClass.set("AppKt")
}
docuconf {
configClass.set("AppConfig")
}2.Declare your configuration
A plain Hoplite data class. Each property reads its name in SCREAMING_SNAKE_CASE: logLevel reads LOG_LEVEL. docuconf adds @Doc, @Min, @Max, @Items, @DurationMin, @DurationMax and @Schemes; a Hoplite Secret is exported as a secret. A KDoc works instead of @Doc: its first sentence is the description and the rest the details.
@DocuconfService(name = "orders")
data class OrdersConfig(
@Doc("HTTP listen port") @Min(1) @Max(65535) val port: Int = 8080,
@Doc("Minimum level of log messages") val logLevel: LogLevel = LogLevel.INFO,
// A Hoplite Secret is exported with `secret: true`; its value never appears in errors or toString.
// @Length(max) on a URL is its maxLength in characters; a longer one fails the boot with out_of_range.
@Doc("Postgres connection URL for the orders database") @Schemes("postgres") @Length(max = 2048) val databaseUrl: Secret,
@Doc("Origins allowed to call the API (CORS)") @Items(min = 1) val allowedOrigins: List<String> = listOf("http://localhost:3000"),
@Doc("Time limit for handling one request") @DurationMin("1s") @DurationMax("5m") val requestTimeout: Duration = Duration.ofSeconds(30),
/**
* Number of background workers that process orders
*
* A KDoc works instead of @Doc: its first sentence is the description, and the rest is the details,
* longer docs for `docuconf docs`. Each worker holds one connection from the pool of [databaseUrl],
* so keep this below the database's connection limit.
*
* - Raise it when the order queue backs up.
* - Lower it when the database is the bottleneck.
*/
@Min(1) @Max(64) val workerCount: Int = 4,
)3.Load it at boot
Docuconf.loadOrExit<T>() checks every variable and file, then lets Hoplite bind the class. On failure it prints every problem and exits 1; Docuconf.load<T>() throws ConfigViolationException instead.
val config = Docuconf.loadOrExit<OrdersConfig>()4.Run it, and see an error
Run the example with a valid environment:
DATABASE_URL=postgres://orders:pw@localhost:5432/orders examples/orders/build/install/orders/bin/ordersWith PORT=0 and no DATABASE_URL, the service refuses to start, lists every problem with its error code and exits 1. The same text goes to /dev/termination-log.
$ PORT=0 examples/orders/build/install/orders/bin/orders
docuconf: 2 configuration problems:
PORT: out_of_range: "0" is below min 1
DATABASE_URL: missing_required: required, but not set5.Test your configuration
Docuconf.load takes an env map, so a test never reads or changes the process environment. This uses kotlin.test on JUnit 5, in a project that applies the dev.docuconf plugin, which indexes the KDoc descriptions.
import dev.docuconf.examples.orders.OrdersConfig
import dev.docuconf.hoplite.Docuconf
import dev.docuconf.kotlin.core.ConfigViolationException
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class OrdersConfigTest {
// Load from an explicit map: System.getenv() is never read.
private fun load(vararg env: Pair<String, String>) = Docuconf.load<OrdersConfig> {
this.env = mapOf(*env)
}
@Test
fun defaults() {
val config = load("DATABASE_URL" to "postgres://orders@db/orders")
assertEquals(8080, config.port)
assertEquals(4, config.workerCount)
}
@Test
fun rejectsBadValues() {
val e = assertFailsWith<ConfigViolationException> { load("PORT" to "70000") }
val codes = e.violations.map { it.input to it.code }
assertTrue("PORT" to "out_of_range" in codes, "$codes")
assertTrue("DATABASE_URL" to "missing_required" in codes, "$codes")
}
}./gradlew test6.Export the contract
The dev.docuconf Gradle plugin's docuconfExport task writes the contract; docuconfCheck, part of check, fails with a diff when the committed file differs from a fresh export.
./gradlew :orders:docuconfExportplugins {
alias(libs.plugins.kotlin.jvm)
application
id("dev.docuconf")
}// docuconfExport rewrites contract.cue; docuconfCheck (part of `check`) fails with a diff when the
// committed file differs from a fresh export. The service name comes from @DocuconfService.
docuconf {
configClass.set("dev.docuconf.examples.orders.OrdersConfig")
}// Code generated by docuconf. DO NOT EDIT.
package orders
import "docuconf.dev/contract"
contract.#Contract & {
apiVersion: "docuconf.dev/v1alpha1"
kind: "ConfigContract"
metadata: {
name: "orders"
generator: {language: "kotlin", sdk: "docuconf-hoplite", version: "0.1.0"}
}
vars: {
ALLOWED_ORIGINS: {
type: "list"
description: "Origins allowed to call the API (CORS)"
configKey: "allowedOrigins"
items: "string"
encoding: "csv"
minItems: 1
default: ["http://localhost:3000"]
}
DATABASE_URL: {
type: "url"
description: "Postgres connection URL for the orders database"
required: true
secret: true
configKey: "databaseUrl"
schemes: ["postgres"]
maxLength: 2048
}
LOG_LEVEL: {
type: "enum"
description: "Minimum level of log messages"
configKey: "logLevel"
values: ["debug", "info", "warn", "error"]
default: "info"
}
PORT: {
type: "int"
description: "HTTP listen port"
configKey: "port"
min: 1
max: 65535
default: 8080
}
REQUEST_TIMEOUT: {
type: "duration"
description: "Time limit for handling one request"
configKey: "requestTimeout"
encoding: "iso8601"
min: "1s"
max: "5m"
default: "30s"
}
WORKER_COUNT: {
type: "int"
description: "Number of background workers that process orders"
details: "A KDoc works instead of @Doc: its first sentence is the description, and the rest is the details,\nlonger docs for `docuconf docs`. Each worker holds one connection from the pool of `databaseUrl`,\nso keep this below the database's connection limit.\n\n- Raise it when the order queue backs up.\n- Lower it when the database is the bottleneck."
configKey: "workerCount"
min: 1
max: 64
default: 4
}
}
}The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.
7.Ktor, http4k and plain JVM services
Call Docuconf.load<AppConfig>() in main before you start the server, and pass the result to Ktor's embeddedServer or your http4k app. A TlsKeyPair file input gives a KeyStore for Ktor's sslConnector. Spring Boot apps should use the Java SDK, which hooks into Spring's own binding.
Next
- The docuconf-kotlin README: every type, file input and option.
- The orders example, with its smoke test.
- The same service in every language, side by side.
- What every SDK supports, and this one's known gaps: JVM target only so far; the core module is Kotlin Multiplatform-ready. No reload: watch.
Every snippet on this page is compiled or run against docuconf-kotlin's main branch in CI. Facts checked 2026-10-08.