skip to content

How does the `by extra` delegate work in the Kotlin DSL for both declaring and reading extra properties, and what are its pitfalls?

level: middleimportance: must knowfreq 45%

answer

  1. by extra(value) = declare; by extra = read
  2. lookup key = property name
  3. read variant casts Any? -> declared type
  4. var by extra is writable -> updates map
  5. has() before get() to avoid exception

basics

~20 s

val x by extra("v") declares an extra property named x with value v. val x: String by extra (no value) reads an existing one. The delegate stores under the property name and casts on read.

solid answer

~50 s

In Kotlin, `extra` is a property delegate provider on `ExtensionAware`. `val springVersion by extra("6.1")` calls the variant that takes an initial value: it registers `springVersion` in the project's `ExtraPropertiesExtension` and binds the local `val` to it. `val springVersion: String by extra` (no argument) is the read variant: it does NOT create anything — it looks up the property by the delegated name and casts the stored `Any?` to the declared type. Because the property name drives the lookup, the local variable name must exactly match the stored key. Pitfalls: (1) reading a missing key throws `UnknownPropertyException` at execution time; (2) the cast can throw `ClassCastException` if the stored type differs; (3) `var x by extra(...)` is writable and reassignment updates the underlying map; (4) capturing the value eagerly at configuration time can break the configuration cache — prefer providers when feeding tasks.

code

kotlin · 16 lines
kotlin
// root build.gradle.kts
val springVersion by extra("6.1.0")
val ci by extra(System.getenv("CI") != null)

// subproject build.gradle.kts — read what root set
val springVersion: String by extra            // name must match
val ci: Boolean by extra

dependencies {
    implementation("org.springframework:spring-core:$springVersion")
}

// safe map-style access when the name can't match
if (project.extra.has("springVersion")) {
    val v = project.extra["springVersion"] as String
}

go deeper

for a junior

Recognise the two forms — by extra(value) to declare, by extra to read — even if delegate internals are fuzzy.

for a middle

Explain that the property name is the lookup key, that read casts Any?, and the common runtime failures (unknown property, ClassCastException).

for a senior

Discuss safe access (has/map form), configuration-cache implications of eager reads, and when to prefer providers.

for a principal

Frame ext usage policy across a multi-module build and steer teams toward typed alternatives (catalogs, convention plugins) over delegate-heavy ext sharing.

## The delegate, precisely Kotlin property delegation lets `val foo by something` route reads (and writes for `var`) through `something`'s `getValue`/`setValue` operators. Gradle's Kotlin DSL adds an `extra` member on `ExtensionAware` that returns such a delegate provider, with two flavours. ### Declaring with an initial value ```kotlin val springVersion by extra("6.1.0") var buildNumber by extra(0) ``` This registers the key (using the **property name**, here `springVersion`/`buildNumber`) into `project.extra` and seeds it with the supplied value. The local `val`/`var` is now a view onto that map entry. For a `var`, `buildNumber = 5` writes straight back into the extension. ### Reading an existing property ```kotlin val springVersion: String by extra ``` No argument is passed, so nothing is created. On first access the delegate calls `project.extra.get("springVersion")` and casts the result to `String`. This is the idiom for reading, in a subproject's `build.gradle.kts`, a value that the root project set. ## The name is load-bearing Because the lookup key is the property's own name, this fails: ```kotlin val version: String by extra // looks up "version", NOT "springVersion" ``` You would need `val springVersion: String by extra`, or the map form `project.extra["springVersion"] as String`. ## Pitfalls | Pitfall | Symptom | |---|---| | Key never set | `ExtraPropertiesExtension.UnknownPropertyException` at execution time | | Wrong declared type | `ClassCastException` on read | | Name mismatch | reads the wrong/absent key silently or throws | | Eager capture | configuration-cache problems / stale values | ## Map-style alternative When you can't make the local name match, drop the delegate: ```kotlin val v = project.extra["springVersion"] as String project.extra["flag"] = true if (project.extra.has("flag")) { /* ... */ } ``` `has(name)` is the safe existence check that avoids the unknown-property exception. ## Relationship to providers Extra properties resolve eagerly to plain values. If a task needs the value, wiring it through a `Provider` (e.g. `providers.provider { project.extra["x"] }`) keeps laziness and is friendlier to the configuration cache than reading the raw value during task configuration.

  • Why must the local Kotlin variable name match the property key when using the read-only `by extra`?
    The delegate has no explicit key argument, so it uses the delegated property's own name as the lookup key in the ExtraPropertiesExtension. A different local name looks up a different (likely missing) key.
  • How do you check whether an extra property exists without triggering an exception?
    Use project.extra.has("name") (or properties.containsKey) before reading; a plain get on a missing key throws UnknownPropertyException.
  • Can you reassign an extra property through the delegate?
    Yes if declared as var by extra(...); reassigning the var writes back into the underlying ExtraPropertiesExtension map.

saying these in an interview costs you the question

  • Saying `val x by extra` (no value) creates the property — it only reads.
  • Thinking the lookup key is independent of the variable name.
  • Claiming the cast in the read delegate is checked at compile time.

context