What is @ReplacedBy used for, and why can't you just rename an input/output property and replace its annotation?
answer
- deprecated getter superseded by a new property
- marks old getter as NOT tracked
- avoids double-counting + validation warning
- like @Internal + documents successor
- 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 linesabstract 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
Be aware @ReplacedBy marks an old, deprecated getter that a newer property replaces.
Explain it stops Gradle tracking the old getter so the value isn't counted twice and no validation warning appears.
Tie it to API-evolution: keep a backward-compatible deprecated getter while the new property carries the tracked value; contrast with @Internal.
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.