Skip to content
docuconf
docuconf on GitHub

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

Every docuconf SDK is a v0.1 alpha and none is on a package registry yet, so step 1 installs from the 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-kotlin
settings.gradle.kts
pluginManagement {
    includeBuild("../docuconf-kotlin/docuconf-gradle-plugin")
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}
includeBuild("../docuconf-kotlin")
build.gradle.kts
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.

OrdersConfig.kt
@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.

Main.kt
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/orders

With 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.

Terminal
$ 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 set

5.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.

OrdersConfigTest.kt
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 test

6.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:docuconfExport
build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.jvm)
    application
    id("dev.docuconf")
}
build.gradle.kts
// 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")
}
contract.cue
// 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

Every snippet on this page is compiled or run against docuconf-kotlin's main branch in CI. Facts checked 2026-10-08.