Skip to main content

OptIn Specification (Draft)

This document is a draft specification that introduces a mechanism to declare and work with additional opt-in requirements that are complementary to Java's visibility modifiers.

note

For a high-level overview of this project, please refer to the User Guide instead.

Motivation

During the evolution of a software application or library, it is sometimes desirable to make select APIs available to users without stability guarantees. A common use-case is gathering feedback for future additions to a library. In the past, popular Java libraries have resorted to notes in the JavaDoc and custom annotations to denote this contract (e.g., Gradle's @Incubating and Guava's @Beta). While some vendors of static analysis tools have integrated warnings for a selection of these annotations of a few popular libraries, there is no standard mechanism for declaring such annotations.

The JDK has solved this problem by introducing Preview Features and, specifically, preview APIs. Since the compiler is aware of preview APIs, it performs strong compile-time checks to ensure that a preview API is not used accidentally. This implementation has two problems: 1. The JDK implementation does not differentiate between APIs in preview, and, more importantly, 2. the mechanism to declare preview APIs is internal to the JDK.

Another use-case not covered by the JDK's preview mechanism is exposing low-level APIs that provide fine-grained control or flexibility in exchange for the guardrails offered by their higher-level counterparts.

Kotlin introduced a flexible mechanism to declare opt-in requirements. This mechanism provides API owners with the ability to require explicit opt-in which users can declare as granular as they wish. The Kotlin compiler checks requirements at compile-time. As the mechanism is based on annotations, it is also fully available to static analysis tools and editors.

The goal of this project is to adapt Kotlin's mechanism for opt-in requirements for Java projects. It should be possible to declare requirements and opt-in into requirements just as it is in Kotlin. Interoperability with Kotlin is strongly desired to benefit from existing opt-in requirements in Java-compatible Kotlin APIs and to respect opt-in requirements from Java APIs in Kotlin in the future. As the Kotlin standard library is quite large and mostly useless in pure Java projects, it is however necessary that the solution does not mandate a dependency on Kotlin's standard library.

To this extent, this specification introduces a minimal set of annotations along with the semantics to declare, opt-in to, and, ultimately, verify opt-in requirements in Java programs.

Requirement Markers

At the core of this specification are requirement markers. Requirement markers are used to mark APIs that require opt-in and referenced when opting into a specific requirement.

An annotation whose declaration is meta-annotated with @com.osmerion.optin.RequiresOptIn is a requirement marker. Requirement markers are defined by API authors and should

The @com.osmerion.optin.RequiresOptIn annotation is defined as follows:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface RequiresOptIn {
String message() default "";
Level level() default Level.ERROR;

enum Level { WARNING, ERROR }
}

The message attribute may be used to provide a custom, human-readable explanation displayed in diagnostics when a requirement is unsatisfied. If unset, or set to its default value (the empty string), a generic message is generated by the verifier.

The level attribute controls the severity of the diagnostic emitted when a requirement is unsatisfied:

  • Level.ERROR (default): Unsatisfied requirements are treated as errors. The program should not be considered well-formed and the verifier must reject the program.
  • Level.WARNING: Unsatisfied requirements are treated as warnings. This may be appropriate for requirements that are advisory rather than mandatory.

Creating requirement markers

A requirement marker is an annotation interface whose declaration is annotated with @RequiresOptIn. For example:

@RequiresOptIn(message = "This API is experimental and may change or be removed in any future release.")
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
public @interface ExperimentalApi {}

Requirement markers must conform to the following constraints on their @Retention and @Target declarations.

@Retention

Requirement markers must have runtime retention. Since the Java compiler assumes RetentionPolicy.CLASS by default, a requirement marker must be meta-annotated with @Retention(RetentionPolicy.RUNTIME). Markers without runtime retention are ill-formed and must be rejected by the verifier.

Runtime retention is required to ensure that compile-time and runtime-based verifiers can operate uniformly. Although the reference verifiers operate at or before compile-time, it must be possible for potential future verifiers to operate at runtime (e.g., via reflection).

@Target

A requirement marker must be meta-annotated with @Target where the set of targets is a non-empty subset of the following supported targets:

ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.FIELD,
ElementType.METHOD,
ElementType.MODULE,
ElementType.PACKAGE,
ElementType.TYPE

A requirement marker with targets outside this set is ill-formed. A requirement marker without an explicit @Target declaration is also ill-formed, since the Java compiler's default set of targets extends beyond the supported subset.

As the Java compiler allows annotations to have targets that are invalid for requirement markers by default, a requirement marker must be meta-annotated with @Target(<targets>) where targets is a subset of the supported targets.

Requirements

An opt-in requirement (or simply requirement) is a constraint placed on the use-sites of an API element that requires callers to make an explicit acknowledgment before the API may be used without diagnostic.

An element is said to have a requirement if it is annotated with a requirement marker, or if it is enclosed by an element that has a requirement (see Requirement Propagation).

When an element with an opt-in requirement is referenced at a use-site, that use-site must either:

  1. Opt in to the requirement using @OptIn (see Declaring Opt-In), or
  2. Propagate the requirement by annotating the enclosing declaration with the same marker annotation. Failing to satisfy either condition at a use-site constitutes a requirement violation. Requirement violations are reported as diagnostics whose severity corresponds to the level declared on the @RequiresOptIn annotation of the marker.

Requirement Propagation

Opt-in requirements are contagious. When a declaration is annotated with a requirement marker, that requirement is transitively imposed on all use-sites of that declaration.

Propagation occurs in the following cases:

  • Enclosing type declarations: All members of a type annotated with a marker annotation inherit that requirement. This includes constructors, fields, methods, and nested types, regardless of whether they are individually annotated.

    @UnstableApi
    public class Unstable {
    void memberMethod() {} // requires opt-in to @UnstableApi

    static class NestedClass {
    void nestedMethod() {} // also requires opt-in to @UnstableApi
    }
    }
  • Enclosing package declarations: All types declared in a package annotated with a marker annotation inherit that requirement.

  • Enclosing module declarations: All types declared in a module annotated with a marker annotation inherit that requirement.

  • Type mentions in signatures: A declaration whose signature mentions a type that requires opt-in also implicitly requires opt-in. For example, a method returning an @UnstableApi-annotated type requires opt-in even if the method itself is not annotated.

A declaration that propagates a requirement by using the marker annotation is said to carry the requirement. This is the preferred approach for API owners building on top of another opted-in API, since it transparently communicates the dependency to downstream users.

Subtyping Requirements

A special form of requirement applies exclusively to the act of subtyping (extending or implementing) a type rather than merely using it. This is expressed via the @SubtypingRequiresOptIn annotation:

@SubtypingRequiresOptIn(MyMarker.class)
public interface MyInterface { ... }

A type annotated with @SubtypingRequiresOptIn is stable to use - callers may reference it, call its methods, and hold references to it without opting in. However, any declaration that extends or implements the annotated type must satisfy the indicated requirement.

This annotation is intended for abstract or open types that are stable from a consumer perspective but risky to implement, such as:

  • Types likely to evolve in a backwards-incompatible manner (e.g., new abstract methods may be added),
  • Types that require careful or complex implementation to be correct,
  • Types not intended for third-party implementations despite being publicly accessible.

@SubtypingRequiresOptIn is itself @Repeatable, allowing multiple independent requirements to be placed on a single type:

@SubtypingRequiresOptIn(ExperimentalApi.class)
@SubtypingRequiresOptIn(InternalExtensionPoint.class)
public abstract class AbstractProcessor { ... }

Restrictions

@SubtypingRequiresOptIn may only be applied to types that permit unrestricted subtyping. Applying it to sealed classes or sealed interfaces is not permitted, since such types already restrict their permitted subtypes at the language level.

Satisfying a Subtyping Requirement

There are three ways to satisfy a subtyping requirement when extending or implementing an annotated type:

  1. Carry the marker: Annotate the subtype with the same marker annotation. This propagates the requirement to all use-sites of the subtype.

    @ExperimentalApi
    public class MyProcessor extends AbstractProcessor { ... }
  2. Propagate via @SubtypingRequiresOptIn: Annotate the subtype with @SubtypingRequiresOptIn using the same marker. This restricts the requirement to the subtype's own subtypes without imposing it on direct use-sites.

    @SubtypingRequiresOptIn(ExperimentalApi.class)
    public abstract class BaseProcessor extends AbstractProcessor { ... }
  3. Opt in locally: Annotate the subtype (or an enclosing declaration) with @OptIn referencing the marker. This acknowledges the requirement without propagating it.

    @OptIn(ExperimentalApi.class)
    public class MyProcessor extends AbstractProcessor { ... }

Declaring Opt-In

The @OptIn annotation allows a declaration or a scope to explicitly acknowledge an opt-in requirement, suppressing diagnostics for use of the corresponding API for the element and the scope spanned by the element (if any) by satisfying the corresponding requirement.

@Documented
@Repeatable(OptIn.Repeated.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.CONSTRUCTOR,
ElementType.FIELD,
ElementType.METHOD,
ElementType.MODULE,
ElementType.PACKAGE,
ElementType.TYPE
})
public @interface OptIn {
Class<? extends Annotation> value();
}

The value element identifies the requirement marker being acknowledged. @OptIn is @Repeatable, allowing a single declaration to opt into multiple independent requirements simultaneously.

The spanned scope of the opt-in acknowledgement depends on the type of the annotated element:

  • For fields, the opt-in applies to the field's initializer.
  • For type declarations, methods, or constructors, the opt-in applies to everything in the corresponding element's lexical scope.
  • For package declarations (in package-info.java), the opt-in applies to all compilation units belonging to the package.
  • For module declarations (in module-info.java), the opt-in applies to all compilation units belonging to the module.