skip to content

What is @ReplacedBy used for, and why can't you just rename an input/output property and replace its annotation?

level: seniorimportance: nice to knowfreq 12%

answer

  1. deprecated getter superseded by a new property
  2. marks old getter as NOT tracked
  3. avoids double-counting + validation warning
  4. like @Internal + documents successor
  5. backward-compat deprecation window

basics

~20 s

@ReplacedBy marks a deprecated getter that has been superseded by a new property, telling Gradle the old getter is NOT an input/output to track. It avoids double-counting the same value during a backward-compatible API migration on a task type.

solid answer

~50 s

`@ReplacedBy` is a migration aid for **task-type API evolution**. When you rename or restructure a tracked property but must keep the **old getter** around for binary/source backward compatibility, that old getter would otherwise still look like an untracked-or-conflicting property to Gradle's annotation processing — and an untracked public getter on a task triggers validation warnings, while leaving it tracked would double-count the same logical value. `@ReplacedBy("newProperty")` tells Gradle: *this getter is deprecated and its value is already represented by the named replacement property, so don't treat it as a separate input/output*. It documents the supersession (the string names the replacement) and suppresses the validation problem without you having to fully delete the old API. You don't 'just rename and re-annotate' because consumers may still call the old getter; `@ReplacedBy` lets both coexist cleanly during the deprecation window.

code

kotlin · 10 lines
kotlin
abstract class Packager : DefaultTask() {
    @get:Input
    abstract val archiveBaseName: Property<String>

    // Old API kept for compatibility; value already tracked via archiveBaseName.
    @Deprecated("Use archiveBaseName", ReplaceWith("archiveBaseName.get()"))
    @get:ReplacedBy("archiveBaseName")
    val outputName: String
        get() = archiveBaseName.get()
}

go deeper

for a junior

Be aware @ReplacedBy marks an old, deprecated getter that a newer property replaces.

for a middle

Explain it stops Gradle tracking the old getter so the value isn't counted twice and no validation warning appears.

for a senior

Tie it to API-evolution: keep a backward-compatible deprecated getter while the new property carries the tracked value; contrast with @Internal.

for a principal

Frame plugin API governance: prescribe @ReplacedBy during deprecation windows to keep cache keys/fingerprints clean while preserving binary/source compatibility.

## The scenario: evolving a task's API Gradle validates that **every public getter on a task** is either: - annotated as a tracked input/output (`@Input`, `@OutputFile`, …), or - explicitly marked **not** to be tracked (`@Internal`, `@ReplacedBy`). An un-annotated public getter is a **validation problem** ("property is not annotated"). Now suppose v1 of your plugin had: ``` @get:Input val outputName: String ``` and v2 renames/redesigns it to `@get:Input val archiveBaseName: Property<String>`. For backward compatibility you can't simply delete `getOutputName()` — existing build scripts call it. So you keep it as a deprecated shim that delegates to the new property. If you leave the old getter `@Input`, Gradle now fingerprints **the same value twice** (once per property), which is wrong and can break up-to-date checks. If you leave it un-annotated, you get a validation warning. ## What @ReplacedBy does ``` @Deprecated("Use archiveBaseName") @get:ReplacedBy("archiveBaseName") val outputName: String get() = archiveBaseName.get() ``` `@ReplacedBy("archiveBaseName")` declares: *this getter is superseded by the property named `archiveBaseName`; do not track it as an input/output of its own.* That: 1. **Suppresses the validation problem** — the getter is now explicitly accounted for. 2. **Avoids double-counting** — the value is tracked only via the replacement property. 3. **Documents the migration** — the annotation argument names the successor, aiding readers and tooling. It is essentially `@Internal` **plus** a documented 'this is the old name of X' link, intended specifically for the deprecation window of a renamed tracked property. ## Why not just rename Renaming and re-annotating the new property is the easy half. The hard part is the **old getter you must keep** for backward compatibility. `@ReplacedBy` exists precisely for that retained-but-deprecated getter so the two can coexist without Gradle either warning about it or fingerprinting it redundantly. ## Relationship to incremental/cacheable correctness Getting this right matters for cacheable/incremental tasks: a redundantly-tracked old getter could perturb the input fingerprint or the cache key, causing spurious cache misses or, worse, the same value influencing the hash twice. `@ReplacedBy` keeps the fingerprint clean during migration.

  • How is @ReplacedBy different from @Internal?
    Both tell Gradle not to track the getter as an input/output. @ReplacedBy additionally documents that the getter is the deprecated former name of a specific replacement property (named in its argument), signalling a migration rather than a permanently-untracked value.
  • Why is leaving the old getter as @Input incorrect after a rename?
    It would fingerprint the same logical value twice — once via the old getter and once via the new property — perturbing the input hash/cache key and risking spurious cache misses or incorrect up-to-date results.

saying these in an interview costs you the question

  • Saying @ReplacedBy tracks the property as an input — it does the opposite.
  • Treating it as a general-purpose 'ignore' annotation rather than a rename/deprecation aid.
  • Claiming you must delete the old getter immediately — the point is backward-compatible coexistence.

context