How are rich version constraints expressed inside a libs.versions.toml entry, and what do require, strictly, prefer, and reject mean there?
answer
- plain string == require
- strictly = hard fail / pin / cap
- prefer = soft tie-breaker only
- reject = exclude bad versions
- object can be in [versions] or inline
basics
~20 sInstead of a plain version string, an entry's version can be an object: version = { require = "..." }, strictly, prefer, or reject. require/strictly set the chosen version (strictly is a hard fail), prefer is a tie-breaker, reject excludes versions.
solid answer
~60 sIn the TOML, any place that takes a version string can instead take a **version object** expressing a rich constraint. The keys are: - **`require`** — a preferred, minimum-acceptable version; conflict resolution may still upgrade it. A bare string `version = "1.5"` is shorthand for `require`. - **`strictly`** — a hard constraint; if resolution wants something outside it, the build **fails** rather than silently upgrading. Useful for pinning. - **`prefer`** — a soft tie-breaker used only when nothing else decides the version. - **`reject`** — a list of versions (or ranges) that must never be selected, e.g. to exclude a known-bad release. ```toml [versions] guava = { strictly = "[33.0, 34.0[", prefer = "33.0.0-jre" } [libraries] slf4j = { module = "org.slf4j:slf4j-api", version = { require = "2.0.0", reject = ["2.0.1"] } } ``` These can appear in a `[versions]` entry or inline in a `[libraries]`/`[plugins]` entry. They let the catalog declare resolution policy, not just a number — though deep resolution semantics belong to constraint resolution proper; here it's about the TOML syntax.
code
toml · 6 lines[versions]
guava = { strictly = "[33.0, 34.0[", prefer = "33.0.0-jre" }
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
slf4j = { module = "org.slf4j:slf4j-api", version = { require = "2.0.0", reject = ["2.0.1"] } }go deeper
Know a version can be a plain string; rich objects are an advanced form.
Distinguish require vs strictly and know prefer is a soft hint.
Explain all four keys, the TOML syntax in [versions] and inline, and when strictly fails the build.
Set policy on where strictly/reject are mandated (security pins) vs forbidden (to keep upgrades fluid) across a shared catalog.
## Plain string vs rich version object The simplest version is a string: `junit = "5.10.2"`. Internally Gradle treats that as a **`require`** constraint. When a single number isn't expressive enough, the catalog lets you replace the string with an inline TOML table holding rich-version keys. ## The four keys - **`require`** — "I need at least this; this is my preferred choice." Still subject to upgrade by conflict resolution if another part of the graph demands higher. This is the default meaning of a plain string. - **`strictly`** — "It must be within this; refuse anything else." If the resolved graph would otherwise pick a version outside the strict constraint, the build **fails** with a conflict, rather than silently choosing a higher version. This is how you pin or cap a dependency. A `strictly` range like `[1.0, 2.0[` caps below 2.0. - **`prefer`** — "If nothing else decides, lean toward this." It's only a tie-breaker and never causes a failure. Often paired with `strictly` to give a range plus a sweet-spot. - **`reject`** — a list of versions/ranges that are forbidden, e.g. to skip a release with a known regression: `reject = ["1.2.3"]`. ## Syntax in the catalog These appear two ways: ```toml # 1) as a named version in [versions], referenced by version.ref [versions] guava = { strictly = "[33.0, 34.0[", prefer = "33.0.0-jre" } [libraries] guava = { module = "com.google.guava:guava", version.ref = "guava" } # 2) inline in a [libraries] entry slf4j = { module = "org.slf4j:slf4j-api", version = { require = "2.0.0", reject = ["2.0.1"] } } ``` Both forms are valid; the `[versions]` form is better when several libraries share the constraint. ## When to reach for each - **`require`/plain string** — the common case; cooperative with the rest of the graph. - **`strictly`** — security or compatibility pins where an unexpected upgrade is unacceptable; it turns silent upgrades into loud build failures. - **`prefer`** — express a target without forbidding alternatives. - **`reject`** — surgically exclude a bad version while staying otherwise flexible. ## Caveats - A `strictly` that conflicts with another module's `strictly` produces an unresolvable conflict and a build failure — that's by design. - Rich versions describe *intent*; the actual selected version is decided by Gradle's conflict resolution across the whole graph. The TOML only declares the constraint. - Avoid over-pinning with `strictly` everywhere; it makes upgrades brittle. Reserve it for deps that genuinely must not move.
- What does a bare `version = "1.5"` actually mean as a constraint?It's shorthand for `require = "1.5"` — a preferred minimum that conflict resolution may still upgrade if another part of the graph needs higher.
- When would strictly cause a build failure?When the resolved dependency graph wants a version outside the strict constraint; instead of silently upgrading, Gradle fails with an unresolvable conflict.
- How do you exclude a single known-bad release while staying flexible otherwise?Use `reject = ["1.2.3"]` alongside a require/strictly; the rejected versions are never selected while others remain eligible.
saying these in an interview costs you the question
- Saying prefer can fail the build — it never does; it's only a tie-breaker.
- Treating a plain string as strictly — it's require.
- Using strictly everywhere, making the catalog brittle to upgrade.