Typed layered JSON and YAML config for Kotlin Multiplatform, JVM, and Android.
Kayan is a Kotlin Gradle plugin that turns layered JSON or YAML config into a typed
Kotlin object. Kotlin Multiplatform, JVM, and Android projects can read that object
without platform-specific BuildConfig wiring.
The name comes from the Arabic word كيان (Kayan), which means "entity", "structure", or "being".
JSON is valid YAML, but Kayan still makes the format choice explicit so your builds do not develop a split personality.
- Keep config in JSON or YAML while exposing a typed Kotlin API.
- Let the consuming app own the schema and generated property names.
- Resolve layered defaults and overrides deterministically at build time.
- Work in shared Kotlin across KMP, JVM, and Android modules.
- Reuse resolved values inside Gradle itself with
buildValue("key").
Add Maven Central to Gradle plugin resolution when consuming the published plugin:
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}Add a config file:
{
"flavors": {
"prod": {
"api_base_url": "https://api.example.com",
"feature_search_enabled": false
},
"dev": {
"api_base_url": "https://dev.example.com",
"feature_search_enabled": true
}
},
"brand_name": "Example App"
}YAML works too:
flavors:
prod:
api_base_url: https://api.example.com
feature_search_enabled: false
dev:
api_base_url: https://dev.example.com
feature_search_enabled: true
brand_name: Example AppApply the plugin and declare your schema:
plugins {
kotlin("multiplatform") version "<kotlin-version>"
id("io.github.mohamadjaara.kayan") version "<kayan-version>"
}
kayan {
packageName.set("sample.generated")
flavor.set("prod")
schema {
string("api_base_url", "API_BASE_URL", required = true)
boolean("feature_search_enabled", "FEATURE_SEARCH_ENABLED")
string("brand_name", "BRAND_NAME")
}
}Generate the config source:
./gradlew generateKayanConfigValidate every flavor and configured target in CI without generating artifacts:
./gradlew validateKayanConfigValidation also runs as part of an existing check lifecycle. It needs only the
Kayan plugin, schema, and input files; Kotlin/Android plugins and generation names
are optional. See the validation guide
for target-aware validation behavior.
Use the generated object from shared code:
import sample.generated.SampleConfig
val baseUrl = SampleConfig.API_BASE_URL
val searchEnabled = SampleConfig.FEATURE_SEARCH_ENABLED
val brandName = SampleConfig.BRAND_NAMEKayan writes generated source to build/generated/kayan/kotlin and adds the
directory to the appropriate source set.
To inspect resolved values and the source file, layer, and key path that supplied each value, run:
./gradlew explainKayanConfig # Uses kayan.flavor
./gradlew explainKayanConfig --flavor prod --target iosThe task prints every declared key, distinguishing <absent> from explicit
null. It uses the same validation and precedence as generation, including
preventOverride, and needs no Kotlin plugin, packageName, or className.
It writes no generated files. See Explaining configuration.
For Kotlin Multiplatform projects that need one config API in shared code but different values per
target, add targets in the config file and configure Kayan targets:
brand_name: Example App
flavors:
prod:
targets:
android:
bundle_id: com.example.android
ios:
bundle_id: com.example.ioskayan {
packageName.set("sample.generated")
flavor.set("prod")
targets("android", "ios")
}Kayan generates an expect object into commonMain and matching actual object declarations into
the configured target source sets.
For non-standard source set names or custom target labels, use the DSL form:
kayan {
targets {
ios()
jvm("desktop")
sourceSet("appleMain", "ios-shared")
}
}Kayan is for non-sensitive build and app configuration. Schema values may appear in generated source or Gradle configuration, so keep API keys, passwords, tokens, and other secrets in dedicated secure storage.
buildValue()is experimental. Opt in with@file:OptIn(io.kayan.gradle.ExperimentalKayanGradleApi::class)when using the Gradle build-time API frombuild.gradle.kts.
When Gradle logic needs the same resolved config, use buildValue() directly in
build.gradle.kts:
@file:OptIn(io.kayan.gradle.ExperimentalKayanGradleApi::class)
val isSearchEnabled =
kayan.buildValue("feature_search_enabled")
.asBoolean()
dependencies {
if (isSearchEnabled) {
implementation("com.example:search-sdk:1.0.0")
}
}Use this for conditional dependencies, task inputs, and other configuration-time
decisions. Provider variants such as asStringProvider() defer resolution for
lazy task wiring.
- Overview
- Quick start
- Gradle usage
- Build-time config access
- Resolution order
- Config file shape
- Schema types
- Validation
- Schema export
- BuildConfig migration
- White-label setup
- Commands
- Threat model
Build everything:
./gradlew buildRun plugin tests:
./gradlew :gradle-plugin:testThe app in sample/ consumes the local plugin and uses its generated config on
desktop, web, and Apple targets.
Run the desktop sample:
./gradlew -p sample runRun the web sample:
./gradlew -p sample wasmJsBrowserDevelopmentRunMore sample details are in sample/README.md.