skip to content

Gradle properties are always strings. How do you safely read a property as a typed, defaulted value (e.g. a boolean flag) and wire it into a task input?

level: seniorimportance: should knowfreq 35%

answer

  1. properties are strings
  2. .map convert, .orElse default
  3. wire Provider into @Input Property
  4. bare -Pflag = empty string, not true
  5. validate inside map, throw GradleException

basics

~20 s

Read it as a Provider<String> via providers.gradleProperty("flag"), transform with .map { it.toBoolean() }, supply a default with .orElse(false), then .set() it into the task's lazy Property. Never assume the property exists or that it's non-string.

solid answer

~40 s

Every gradle property arrives as a **String** (or absent). The robust pattern keeps it lazy end-to-end: `providers.gradleProperty("release")` gives a `Provider<String>`; `.map { it.toBoolean() }` converts it to `Provider<Boolean>` only when resolved; `.orElse(false)` supplies a default if the property is missing; and you wire the result into a task's `Property<Boolean>` with `.set(...)`. Because nothing is `.get()`-ed at configuration time, the conversion is deferred, configuration-cache-safe, and the property is tracked as a declared input — change it and the task re-runs. The anti-pattern is `project.findProperty("release")?.toString()?.toBoolean() ?: false` evaluated eagerly into a plain `Boolean`: it reads `project`, resolves immediately, and isn't wired as an input. For parsing failures, do the conversion inside `.map { }` so the error is attributed to that property, and validate (e.g. throw `GradleException`) when the string is malformed.

code

kotlin · 7 lines
kotlin
val release = providers.gradleProperty("release")
    .map { it.toBoolean() }      // String -> Boolean, lazily
    .orElse(false)               // default when absent

tasks.register<Jar>("distJar") {
    archiveClassifier.set(release.map { if (it) "" else "snapshot" })
}

go deeper

for a junior

Know properties are strings and you must convert and default them.

for a middle

Use .map/.orElse and wire into a task Property; know toBoolean() only accepts 'true'.

for a senior

Keep the whole chain lazy and CC-safe, validate inside map, handle the bare-flag empty-string case.

for a principal

Establish a shared helper/convention for typed, validated property reads so toggles behave consistently and cache-correctly across all modules.

## The core constraint Gradle stores project properties as **strings**. `-Pcount=3` gives you `"3"`, not `3`. So typed access is always *read string → convert → default*. ## The lazy, CC-safe pattern ```kotlin abstract class PackageTask : DefaultTask() { @get:Input abstract val release: Property<Boolean> @get:Input abstract val parallelism: Property<Int> @TaskAction fun run() { logger.lifecycle("release=${release.get()} parallelism=${parallelism.get()}") } } tasks.register<PackageTask>("package") { release.set( providers.gradleProperty("release").map { it.toBoolean() }.orElse(false) ) parallelism.set( providers.gradleProperty("parallelism").map { it.toInt() }.orElse(1) ) } ``` Walk through it: - `providers.gradleProperty("release")` → `Provider<String>`, lazy. - `.map { it.toBoolean() }` → `Provider<Boolean>`; the lambda runs only when resolved. - `.orElse(false)` → value when the property is absent. - `.set(...)` wires the provider into the task's `@Input Property`, so Gradle fingerprints it as an input and the conversion never runs at configuration time. ## Validation & error attribution Do conversion inside `.map`, and fail loudly on garbage: ```kotlin val count = providers.gradleProperty("count").map { it.toIntOrNull() ?: throw GradleException("-Pcount must be an integer, got '$it'") }.orElse(1) ``` Because the lambda runs lazily, the exception surfaces with a clear message when the value is actually needed. ## Booleans: the `"false"` trap `String.toBoolean()` returns true **only** for `"true"` (case-insensitive) and false for everything else — including `"yes"`, `"1"`, and empty. Decide your accepted spellings explicitly if `-Pflag=1` must mean true. Also note `-Pflag` with no `=value` yields an **empty string**, not `"true"`; so a bare `-Pflag` is `""` → `toBoolean()` = false. Handle presence-as-true explicitly if that's your intent: `.map { true }` keyed on presence, or check `getOrNull() != null`. ## Anti-patterns to avoid - Eager: `val r = (project.findProperty("release") as String?)?.toBoolean() ?: false` — reads `project`, resolves now, not an input. CC-hostile. - `.get()` at configuration time on an optional property — throws when absent. - Storing the converted value in a captured `val` and referencing it from a task action — captures configuration-time state instead of wiring a provider. ## Why it matters at senior level This pattern is the seam between human-supplied flags and Gradle's lazy/incremental machinery. Get it right and toggles are cache-correct, re-run on change, and never crash configuration; get it wrong and you silently break the configuration cache or get stale outputs.

  • What value does providers.gradleProperty("flag") hold when the user passes a bare -Pflag with no =value?
    An empty string "", not "true". So "".toBoolean() is false. If presence should mean true, key on presence (getOrNull() != null) rather than parsing the string.
  • Where should the string-to-int conversion live so a bad value gives a clear error?
    Inside the provider's .map { } lambda (e.g. it.toIntOrNull() ?: throw GradleException(...)). Because it resolves lazily, the error is attributed to that property and surfaces when the value is used.
  • Why not just convert eagerly into a plain Boolean val at configuration time?
    That reads project/resolves immediately, isn't tracked as a declared input, and is hostile to the configuration cache. Keep it a Provider and wire it into the task's Property.

saying these in an interview costs you the question

  • Assuming a property is already a non-string type or that bare -Pflag equals "true".
  • Using String.toBoolean() and expecting "1"/"yes" to be true.
  • Eagerly resolving into a captured val and referencing it from the task action (breaks CC and incrementality).

context