Reference of all styles

Llamas were cooking AI slop, but no worries, they’re fed.
This page was generated by an LLM based on Javadoc comments, commit messages, and issue-tracker discussions.
Some parts may be imprecise or of diminishing practicality, but we hope it’s still more useful than not.

Reference

This reference documents all available @Value.Style annotation attributes that control code generation for immutable value types. Style attributes allow you to customize naming conventions, builder behavior, visibility, validation, and many other aspects of generated code.

Mostly inline styles are shown, i.e. right on the class or interface, but it doesn’t mean you should do the same. Styles can be applied at different levels, so you can consolidate your styles:

  • Package level or Parent package level - Affects all classes in the package (and nested packages unless overridden)
  • As meta-annotation - Create custom style annotations for reuse across your codebase

See


add

Default: "add*"

Description: Naming template for builder methods that add a single value to a collection attribute from an iterable.

This style attribute controls the naming of the additive initializer methods generated on builders for collection attributes. The asterisk (*) in the template is replaced with the attribute name.

@Value.Style(add = "with*Appended")
@Value.Immutable
interface Shopping {
  List<String> items();
}

// Generated usage:
ImmutableShopping.builder()
  .withItemsAppended("apple")
  .withItemsAppended("banana")
  .build();

When depluralize is enabled, the attribute name will be automatically converted to singular form. For example, items() becomes addItem() instead of addItems().

Related: This template works in conjunction with the collection attribute detection. The generated method accepts a single element of the collection’s component type and adds it to the underlying collection being built.

See also: addAll, depluralize, init


addAll

Default: "addAll*"

Description: Naming template for builder methods that add all values from an iterable to a collection attribute.

This generates bulk additive initializer methods on builders for collection attributes. Unlike add which adds one element at a time, this method accepts any Iterable of the collection’s component type and adds all elements in a single call.

@Value.Style(addAll = "with*Collection")
@Value.Immutable
interface Shopping {
  List<String> items();
}

// Generated usage:
ImmutableShopping.builder()
  .withItemsCollection(Arrays.asList("apple", "banana"))
  .build();

The generated method accepts Iterable<? extends E> where E is the element type, providing flexibility in accepting various collection types while maintaining type safety.

See also: add, init


addAllBuilder

Default: "addAll*Builders"

Description: Naming template for builder methods that add a collection of nested builders to a collection attribute.

This style attribute is only effective when attributeBuilderDetection is enabled. It controls the naming of methods that accept multiple builder instances at once for collection attributes whose element type has a discoverable builder.

The generated method will accept an Iterable of builder instances and add all of them to the internal collection of builders. This provides a convenient way to initialize a collection of nested immutable objects when you already have builder instances.

@Value.Style(
  attributeBuilderDetection = true,
  addAllBuilder = "addAll*Builders"
)
@Value.Immutable
interface Team {
  List<Member> members();
}

// Generated usage:
List<ImmutableMember.Builder> memberBuilders = ...;
ImmutableTeam.builder()
  .addAllMembersBuilders(memberBuilders)
  .build();

See also: attributeBuilderDetection, addBuilder, getBuilders


addBuilder

Default: "add*Builder"

Description: Naming template for builder methods that add a new builder instance to a collection of nested builders.

This style attribute is only applicable when attributeBuilderDetection is enabled. The generated method returns a builder instance for the collection’s element type, allowing you to configure nested immutable objects inline using a fluent builder API.

When you call this method, it creates a new builder, adds it to an internal list of builders, and returns it for further configuration. This enables a more fluent and readable way to build collections of nested immutable objects without explicitly creating and managing builder instances.

@Value.Style(attributeBuilderDetection = true)
@Value.Immutable
interface Parent {
  List<Child> children();
}

@Value.Immutable
interface Child {
  String name();
  int age();
}

// Generated usage - fluent nested building:
ImmutableParent.builder()
  .addChildBuilder()
    .name("Alice")
    .age(10)
  .addChildBuilder()
    .name("Bob")
    .age(12)
  .build();

// Also accepts pre-built builder:
ImmutableChild.Builder childBuilder = ImmutableChild.builder().name("Charlie").age(8);
ImmutableParent.builder()
  .addChildBuilder(childBuilder)
  .build();

See also: attributeBuilderDetection, getBuilder, addAllBuilder


additionalJsonAnnotations

Default: {} (empty array)

Description: List of additional annotations to pass through for any Jackson JSON object methods and classes.

Status: This attribute is soft-deprecated and no longer necessary in most use cases. Modern Immutables versions have improved Jackson integration that handles annotation propagation automatically through the passAnnotations mechanism.

Historically, this was used to ensure certain Jackson annotations were applied to generated JSON serialization infrastructure. With current versions, you should prefer using passAnnotations for a more general annotation propagation mechanism.

@Value.Style(additionalJsonAnnotations = {JsonInclude.class})

Migration Note: If you’re currently using this attribute, consider migrating to passAnnotations which provides broader control over annotation propagation to generated code.

See also: jacksonIntegration, passAnnotations, forceJacksonPropertyNames


additionalStrictContainerConstructor

Default: true

Description: Controls generation of additional “strict” constructor overloads that accept exact container types as parameters alongside more lenient overloads accepting supertypes.

Constructors or factory methods (of()) can have alternative overloaded versions with more strict parameter types matching the abstract attribute types exactly. For example, if you have a constructor parameter attribute of type List<T>, two constructor methods will be generated:

  1. A “strict” constructor accepting List<T> as parameter (matches the declared type exactly)
  2. A “lenient” constructor accepting Iterable<T> as parameter (accepts any iterable and converts to immutable list)

When set to false, only the lenient overload is generated, reducing code size when the strict overload is redundant or unnecessary.

@Value.Style(
  allParameters = true,
  additionalStrictContainerConstructor = false  // Disable strict overload
)
@Value.Immutable
interface Point {
  List<Integer> coordinates();
}

// With true (default): Both overloads available
Point.of(List.of(1, 2, 3))              // Strict: accepts List<Integer>
Point.of(Arrays.asList(1, 2, 3))        // Lenient: accepts Iterable<Integer>

// With false: Only lenient version generated
Point.of(someIterable)                   // Accepts any Iterable<Integer>

When to disable: Set this to false for backward compatibility with versions prior to the introduction of this feature, or to reduce generated code size when you don’t need the strict type safety of exact container type matching.

Related Issues:

  • #1603: Compilation error when using this feature with stagedBuilder and generic types in version 2.11.5
  • #1433: Discussion about double injection issues with container fields

Note: If no container parameters are present in constructors, no additional overloaded version will be generated regardless of this setting.

See also: allParameters, allMandatoryParameters, of, stagedBuilder


allowedClasspathAnnotations

Default: {} (empty array - allows all discovered annotations)

Description: Provides a whitelist for controlling which auto-discovered annotations Immutables should apply to generated code.

Immutables applies various complementary annotations if found on the classpath during annotation processing. These include:

  • @org.immutables.value.Generated
  • @javax.annotation.Generated / @java.annotation.processing.Generated
  • @javax.annotation.concurrent.Immutable
  • @javax.annotation.ParametersAreNonnullByDefault
  • @javax.annotation.CheckReturnValue
  • @edu.umd.cs.findbugs.annotations.SuppressFBWarnings
  • @com.google.errorprone.annotations.Var / @com.google.errorprone.annotations.Immutable
  • …and others

This annotation attribute provides a whitelist (when not empty) to control which discovered annotations should be applied. This is particularly useful because build configurations can become complex with different build tools and IDEs, making classpath management alone insufficient for controlling auto-applied annotations.

By default (empty array): All auto-discovered annotations are allowed and will be applied if found on classpath.

When non-empty: Only the specified annotation types will be applied if discovered on classpath.

@Value.Style(
  // Disable all auto-discovered annotations by specifying a dummy annotation
  allowedClasspathAnnotations = {java.lang.Override.class}
)

Disabling all auto-discovery: To simply disable all auto-discovered annotation application, specify some standard annotation like java.lang.Override. The array will no longer be empty, so only Override would be eligible (which doesn’t make sense for generated classes), effectively disabling the feature.

Important Notes:

  • Standard java.lang annotations like @Override, @Deprecated, @SuppressWarnings are always applied where supported, regardless of this configuration
  • javax.annotation.* and java.annotation.processing.* annotations ARE configurable by this attribute
  • Annotations propagated from abstract value types or attributes to generated code via other mechanisms (like passAnnotations) are not regulated by this property

Alternative approach - Classpath fence: You can also inhibit classpath entries from discovery using the META-INF extension mechanism. Add unwanted fully qualified class name prefixes (e.g., full class names or package names ending with a dot) as lines to the META-INF/extensions/org.immutables.inhibit-classpath resource file available in your classpath. This mechanism provides a general classpath discovery blacklist and overrides any whitelist specified here.

Related Issues:

  • #1138: Issues with suppressing javax.annotation.Generated and the interaction between allowedClasspathAnnotations and META-INF inhibit mechanism
  • #1403: Request for Jakarta EE annotation support (see jakarta style attribute)
  • #1192: Visibility issues with generated modifiable classes related to annotation configuration

Example (value-fixture/src/org/immutables/fixture/style/LightOnAnnotations.java:15):

@Value.Style(
  generateSuppressAllWarnings = false,
  allowedClasspathAnnotations = {Override.class}
)

See also: generateSuppressAllWarnings, jakarta, passAnnotations


allMandatoryParameters

Default: false

Description: When enabled, mandatory attributes are automatically propagated to become parameters of the value object constructor.

This style flag provides a convenient way to generate constructor-based initialization for all required (mandatory) attributes without manually annotating each one with @Value.Parameter. Mandatory attributes are those that:

  • Are not annotated with @Value.Default
  • Are not annotated with @Nullable or similar nullable annotations
  • Are not optional (e.g., Optional<T>) or collection types (which default to empty)
  • Are not derived (@Value.Derived) or lazy (@Value.Lazy)

When enabled, a static factory method (typically named of()) or public constructor will be generated accepting all mandatory attributes as parameters, allowing for concise object construction.

@Value.Style(allMandatoryParameters = true)
@Value.Immutable
interface Person {
  String name();                    // Mandatory - becomes parameter
  int age();                        // Mandatory - becomes parameter
  Optional<String> nickname();      // Optional - not a parameter
  @Value.Default
  boolean active() { return true; } // Has default - not a parameter
}

// Generated usage:
Person person = ImmutablePerson.of("John", 30);
// Builder still available unless disabled via @Value.Immutable(builder = false)

Important: This parameter conflicts with allParameters and is ignored when allParameters is enabled. Use allParameters when you want ALL attributes (including optional and defaulted ones) to become parameters.

Use Case: This is particularly useful for domain value objects where you want the convenience of constructors for required fields while maintaining builder access for optional configuration:

// Construction with mandatory parameters
Person person = ImmutablePerson.of("Alice", 25)
  .withNickname(Optional.of("Ali"));

Related Issues:

  • #1444: @Builder.Constructor does not support allMandatoryParameters and other style parameters
  • #637: Discussion about parameter ordering in inherited immutable abstract classes with allParameters

See also: allParameters, of, @Value.Parameter


allParameters

Default: false

Description: When enabled, all settable attributes are considered as if they are annotated with @Value.Parameter, enabling construction via factory method or constructor with all attributes as parameters.

This style flag transforms immutable value objects into tuple-like structures that are constructed entirely through parameters rather than builders. All attributes become constructor/factory parameters unless explicitly opted out using @Value.Parameter(false).

This is particularly useful for creating simple value types, tuples, or data transfer objects where the builder pattern adds unnecessary verbosity.

@Value.Style(
  typeImmutable = "*Tuple",
  allParameters = true,
  defaults = @Value.Immutable(builder = false)  // Often combined to disable builder
)
@Value.Immutable
interface RgbDef {
  double red();
  double green();
  double blue();
  @Value.Parameter(false)  // Explicitly opt-out of parameter
  List<String> tags();
}

// Generated usage:
RgbTuple color = RgbTuple.of(0.5, 0.3, 0.8);  // Concise tuple-style construction
// Tags must be set via builder (if not disabled) or withTags() method

Opting out: Use @Value.Parameter(false) on specific attributes to exclude them from the parameter list. These attributes will still have builder initializers and with-copy methods generated.

Common pattern - Tuple-style annotations: This style is often used to create custom meta-annotations for tuple-like types:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@Value.Style(
    typeAbstract = "*Def",
    typeImmutable = "*",
    allParameters = true,
    visibility = Value.Style.ImplementationVisibility.PUBLIC,
    defaults = @Value.Immutable(builder = false))
public @interface Tupled {}

// Usage:
@Tupled
interface ColorDef {
  double red();
  double green();
  double blue();
}

Color color = Color.of(0.9, 0.7, 0.4);

Interaction with other styles:

Related Issues:

  • #794: Request for static constructors to work with deepImmutablesDetection
  • #793: Staged and strict builders compatibility with deepImmutablesDetection
  • #795: Request for recursive deepImmutablesDetection
  • #637: Parameter ordering in inherited immutable abstract classes

Example (value-fixture/src/org/immutables/fixture/style/Tuple.java:21): Demonstrates tuple-style objects with all parameters and no builder.

See also: of, allMandatoryParameters, deepImmutablesDetection, @Value.Parameter


alwaysPublicInitializers

Default: true

Description: Controls whether all builder initializer methods (setter methods) are forced to have public visibility regardless of the attribute accessor’s visibility level.

All initializers (the methods to set attribute values on a builder) usually have public visibility regardless of the visibility of the attribute accessors. This default behavior ensures that builders are maximally accessible and easy to use, which is appropriate for most use cases where the builder is the primary way to construct objects.

However, in some scenarios, especially with abstract classes having fine-grained access control, you may want initializers to respect the access level of the attribute accessors. Setting this flag to false makes builder initializers follow the same visibility as their corresponding attribute accessors.

When true (default):

  • All builder initializers are public
  • Most common and convenient for typical usage
  • Works well with interfaces where all accessors are implicitly public

When false:

  • Builder initializers follow attribute accessor visibility
  • Useful for abstract classes with access control requirements
  • Enables encapsulation of internal configuration details
@Value.Style(alwaysPublicInitializers = false)
@Value.Immutable
abstract class Config {
  public abstract String publicValue();
  abstract String packageValue();           // package-private
  protected abstract String protectedValue();
}

// Generated builder methods will have matching visibility:
ImmutableConfig.Builder builder = ImmutableConfig.builder();
builder.publicValue("visible");     // public - accessible everywhere
builder.packageValue("internal");   // package-private - only in same package
builder.protectedValue("guarded");  // protected - subclasses and same package

Important limitations:

  • This flag is disregarded when stagedBuilder is enabled. Staged builders generate stage interfaces which require public methods to function correctly, so initializers will be public regardless of this setting.
  • Usually doesn’t matter for value types defined as interfaces (where all accessors are implicitly public)

Use cases:

  • Abstract classes with internal/protected attributes that shouldn’t be publicly settable
  • Domain models with fine-grained encapsulation requirements
  • Builder inheritance scenarios where visibility control is important

Related Issues:

  • #790: Follow-up discussion about protected builder methods and visibility control
  • #918: How to mark fields package-private
  • #664: Request for separate type and method visibility controls

Example (value-fixture/src/org/immutables/fixture/style/NonPublicInitializers.java:10):

@Value.Style(alwaysPublicInitializers = false)
@Value.Immutable
abstract class SampleValue {
  public abstract int publicField();
  protected abstract int protectedField();
  abstract int packagePrivateField();
}

See also: init, builderVisibility, stagedBuilder, visibility


attributeBuilder

Default: ["Builder", "*Builder", "builder", "from", "build", "*Build", "new"]

Description: Pattern array for detecting builder methods or constructors on attribute types when attributeBuilderDetection is enabled.

This style attribute defines patterns used to discover builders on immutable attribute types. The patterns are applied to both static and instance methods of the attribute type to find builder factory methods.

Detection mechanism: The processor scans attribute types looking for methods matching these patterns:

  • Static methods: Methods like static Builder builder() or static SomeBuilder from()
  • Instance methods: If only instance methods match, the builder class must have a public no-arg static construction method or constructor
  • Special token "new": Indicates that a public no-arg constructor should be used for builder instantiation

Pattern matching: The asterisk (*) acts as a wildcard. For example:

  • "builder" matches exactly builder()
  • "*Builder" matches fooBuilder(), barBuilder(), etc.
  • "Builder" matches exactly Builder() (constructor or method name)
  • "new" is a special keyword indicating constructor-based instantiation
// Example 1: Type with discoverable builder via static method
class MyObject {
  static Builder builder() { return new Builder(); }

  static class Builder {
    public Builder() {}
    public Builder prop(String value) { return this; }
    public MyObject build() { return new MyObject(); }
  }
}

// Example 2: Type requiring "new" pattern
class MyObject {
  class Builder {
    public Builder() {}  // Requires "new" in attributeBuilder pattern
    public Builder(MyObject copy) {}
    MyObject build() { return new MyObject(); }
  }
}

@Value.Style(
  attributeBuilderDetection = true,
  attributeBuilder = {"builder", "from", "new"}  // Customize detection pattern
)

Important notes:

  • This is a detection pattern, not a formatting pattern. It defines how to recognize builders, not how to name generated methods.
  • Only applies when attributeBuilderDetection is true
  • The default patterns cover most common builder naming conventions

Use case: Customize this when your attribute types use non-standard builder naming conventions or when you want to restrict which builders are auto-detected.

See also: attributeBuilderDetection, getBuilder, setBuilder


attributeBuilderDetection

Default: false

Description: When enabled, generates additional builder API for immutable attributes whose types have discoverable builders, enabling fluent nested object construction.

This powerful feature allows you to build nested immutable objects directly within the parent builder without explicitly creating and managing separate builder instances. The processor automatically detects when attribute types have builders (using patterns from attributeBuilder) and generates convenient methods for working with those nested builders.

Generated API for single child attributes:

  • BuilderT *Builder() - Get or create builder for the attribute (idempotent in strict mode)
  • ParentT *Builder(BuilderT builder) - Set the attribute using an existing builder instance

Generated API for collection of children:

  • BuilderT add*Builder() - Add a new builder to the collection and return it for configuration
  • ParentT addAll*Builder(Iterable<BuilderT>) - Add multiple builder instances from an iterable
  • ParentT addAll*Builder(BuilderT...) - Add multiple builder instances via varargs
  • List<BuilderT> *Builders() - Get the list of all builders added so far

How builder discovery works: To discover builders on value attributes, value methods are scanned for patterns specified in attributeBuilder. This applies to both static and instance methods. If a builder is only discoverable through an instance method, the builder class must have a public no-arg static construction method or constructor (detected using the special "new" token).

@Value.Style(attributeBuilderDetection = true)
@Value.Immutable
interface Order {
  Customer customer();
  List<Item> items();
}

@Value.Immutable
interface Customer {
  String name();
  String email();
  static Builder builder() { return new Builder(); }
}

@Value.Immutable
interface Item {
  String name();
  double price();
  static Builder builder() { return new Builder(); }
}

// Generated usage - fluent nested building:
Order order = ImmutableOrder.builder()
  .customerBuilder()       // Returns Customer.Builder
    .name("John Doe")
    .email("john@example.com")
  .addItemBuilder()        // Returns Item.Builder, adds to items collection
    .name("Apple")
    .price(1.50)
  .addItemBuilder()        // Another Item.Builder
    .name("Banana")
    .price(0.75)
  .build();

Behavior in strict mode: When combined with strictBuilder:

  • For single attributes: You can only set the builder once via *Builder(BuilderT builder)
  • The *Builder() getter can be called multiple times, returning the same builder instance
  • Once a nested immutable is set, it cannot be reset (enforcing forward-only builder usage)

Builder requirements: For a type to be recognized as having a discoverable builder:

  1. Must have a method matching one of the attributeBuilder patterns
  2. If only instance methods match, must have public no-arg constructor or static factory
  3. The builder must have a build() method returning the immutable type

Performance note: This is an experimental feature that increases processing time as it requires deep analysis of attribute types. It only works for types that are already generated or available during annotation processing (not for yet-to-be-generated types).

Example (value-fixture/src/org/immutables/fixture/builder/attribute_builders/FirstPartyImmutableWithDifferentStyle.java:23):

@Value.Style(
  attributeBuilderDetection = true,
  builder = "getTheBuilder",
  build = "doIIT"
)
@Value.Immutable
interface Value {
  FirstPartyImmutable nested();
}

Note: This style parameter is experimental and may change in future versions. It provides an alternative to manually creating and passing builder instances, making code more readable and fluent.

See also: attributeBuilder, getBuilder, setBuilder, addBuilder, addAllBuilder, getBuilders, strictBuilder


attributelessSingleton

Default: false

Description: Enables automatic singleton generation for immutable types that have no attributes defined, reverting to pre-2.1.x behavior.

In versions prior to 2.1, when an immutable type had no attributes defined, Immutables automatically generated it as a singleton with an instance accessor method. This implicit behavior was changed in version 2.1 to require explicit @Value.Immutable(singleton = true) annotation for better clarity and intentionality.

Setting attributelessSingleton = true restores the old automatic behavior for backward compatibility.

When true:

  • Empty immutable types (no attributes) are automatically generated as singletons
  • An accessor method (typically named of() per instance style) provides access to the singleton
  • Only one instance of the immutable object ever exists

When false (default):

  • Explicit @Value.Immutable(singleton = true) is required for singleton generation
  • Empty immutable types generate normally without special singleton handling
@Value.Style(attributelessSingleton = true)
@Value.Immutable
interface EmptyMarker {
  // No attributes defined
}

// Generated usage:
EmptyMarker marker = ImmutableEmptyMarker.of();  // Singleton instance
EmptyMarker same = ImmutableEmptyMarker.of();    // Returns same instance
assert marker == same; // true

Recommendation: As of version 2.1+, the explicit annotation approach is preferred:

@Value.Immutable(singleton = true)  // Explicit and clear
interface EmptyMarker {
  // No attributes
}

Use case: This style flag is primarily useful for:

  • Migrating legacy code from pre-2.1 versions
  • Codebases with many empty marker/tag types where explicit singleton annotation would be verbose
  • Maintaining backward compatibility during version upgrades

Note: This only affects types with zero attributes. If any attributes exist (including derived or lazy), this flag has no effect.

See also: @Value.Immutable(singleton = true), instance


beanFriendlyModifiables

Default: false

Description: When enabled, modifiable companion types generate void setter methods instead of fluent setters, providing partial JavaBean compatibility.

By default, modifiable implementations generate fluent setter methods that return this, allowing for method chaining. When this flag is enabled, setters return void instead, following the traditional JavaBean setter convention.

This style applies only to modifiable companion types generated with @Value.Modifiable, not to builders or immutable implementations.

When false (default):

  • Setters return this for method chaining
  • Enables fluent API style: obj.setName("x").setValue(42)

When true:

  • Setters return void
  • Traditional imperative style: obj.setName("x"); obj.setValue(42);
@Value.Style(beanFriendlyModifiables = true)
@Value.Immutable
@Value.Modifiable
interface Bean {
  String name();
  int value();
}

// Generated modifiable usage with bean-friendly setters:
ModifiableBean bean = ModifiableBean.create();
bean.setName("test");    // void return, not fluent
bean.setValue(42);       // void return

// Compare to default fluent style:
// ModifiableBean bean = ModifiableBean.create()
//   .setName("test")
//   .setValue(42);

Important disclaimer: Immutables is not a JavaBean-compliant toolkit. This style flag only provides superficial compatibility with JavaBean setter conventions. It does not provide:

  • Full JavaBean specification compliance
  • Property change events
  • Introspection support
  • No-arg constructor guarantees
  • Other JavaBean infrastructure

Use cases:

  • Integration with legacy frameworks expecting JavaBean-style setters
  • Code generation tools that require void setters
  • Team preferences for imperative setter style
  • Compatibility with certain serialization libraries

Note: This style makes modifiables slightly less convenient to use but may be required for compatibility with certain frameworks or coding standards.

See also: set, typeModifiable, @Value.Modifiable


build

Default: "build"

Description: Naming template for the instance creation method on the builder that constructs and returns the immutable object.

This style attribute controls the name of the final method called on a builder to create the immutable instance. The asterisk (*) in the template can be replaced with the type name if desired.

@Value.Style(build = "create")
@Value.Immutable
interface Item {
  String name();
}

// Generated usage:
ImmutableItem item = ImmutableItem.builder()
  .name("Apple")
  .create();  // Instead of build()

Using type name in template:

@Value.Style(build = "build*")  // Type name inserted at *
@Value.Immutable
interface Person {
  String name();
}

// Generated usage:
ImmutablePerson person = ImmutablePerson.builder()
  .name("John")
  .buildPerson();  // Method name includes type name

Common naming choices:

  • "build" (default) - Standard builder pattern naming
  • "create" - Alternative common naming
  • "construct" - More explicit about construction
  • "build*" - Include type name for clarity in context
  • "make*" - Another variant

Method behavior: The build method:

  1. Validates that all mandatory attributes have been set
  2. Throws throwForInvalidImmutableState exception if validation fails
  3. Invokes any @Value.Check validation methods
  4. Constructs and returns the immutable instance

Example (value-fixture/src/org/immutables/fixture/style/AbstractValueNamingDetected.java:25):

@Value.Style(build = "build*")  // Type name inserted at *
@Value.Immutable
interface ValueNamingDetected {
  int value();
}

// Usage:
ImmutableValueNamingDetected.builder()
  .value(42)
  .buildValueNamingDetected();  // Type name in method

See also: builder, buildOrThrow, canBuild, throwForInvalidImmutableState


buildOrThrow

Default: "" (empty, feature disabled)

Description: Naming template for a builder method that accepts an exception factory function, allowing custom exception creation when mandatory attributes are missing.

By default, when calling build() on an incomplete builder (missing mandatory attributes), a standard exception (configured via throwForInvalidImmutableState) is thrown with a generic message. The buildOrThrow method provides an alternative that lets you create a custom exception with context-specific information.

How it works: When enabled (by providing a non-empty template), a method is generated that:

  1. Accepts a Function<List<String>, RuntimeException> parameter (Java 8+) or Function<List<String>, RuntimeException> from Guava (Java 7)
  2. Checks if all mandatory attributes are set
  3. If incomplete, calls your function with the list of missing attribute names
  4. Throws the exception your function returns
  5. If complete, builds and returns the immutable instance

Feature flag: A non-empty template both names the method AND enables generation of this method. Empty string (default) disables the feature.

@Value.Style(buildOrThrow = "buildOrThrow")
@Value.Immutable
interface Config {
  String host();
  int port();
  Optional<String> username();
}

// Generated usage with custom exception:
Config config = ImmutableConfig.builder()
  .host("localhost")
  // port not set - mandatory!
  .buildOrThrow(missingFields ->
    new ConfigurationException(
      "Configuration incomplete. Missing: " + String.join(", ", missingFields)
    )
  );
// Throws: ConfigurationException("Configuration incomplete. Missing: port")

Requirements:

  • Java 8+ or Guava on Java 7 for Function type support
  • Your exception factory function must return a RuntimeException (or subclass)
  • The exception class should have a constructor accepting a String message

Use cases:

  • Domain-specific exceptions (e.g., ValidationException, ConfigurationException)
  • Exceptions that need additional context beyond just the message
  • Integration with validation frameworks
  • Custom error handling strategies

Example (value-fixture/src/org/immutables/fixture/style/SpecifiedException.java:23):

@Value.Style(
  buildOrThrow = "buildOrThrow",
  strictBuilder = true,
  throwForInvalidImmutableState = SampleRuntimeException.class
)
@Value.Immutable
interface SpecifiedExceptionStyle {
  String value();
}

// Usage:
value = ImmutableSpecifiedExceptionStyle.builder()
  .buildOrThrow(missing -> new CustomException("Missing: " + missing));

Advanced usage: The function receives a List<String> containing the names of all missing mandatory attributes, allowing you to create detailed error messages or even collect multiple validation errors.

Note: This feature is experimental and may change in future releases. The method signature or behavior might be adjusted based on user feedback.

See also: build, throwForInvalidImmutableState, validationMethod, canBuild


builder

Default: "builder"

Description: Naming template for the builder factory method. The special keyword "new" can be used to generate a public constructor instead of a factory method.

This style attribute controls how builders are instantiated. By default, a static factory method is generated, but you can customize its name or switch to constructor-based instantiation.

Factory method approach (default):

@Value.Style(builder = "newBuilder")
@Value.Immutable
interface Item {
  String name();
}

// Generated usage:
ImmutableItem item = ImmutableItem.newBuilder()
  .name("Apple")
  .build();

Constructor approach using “new” keyword:

Since version 2.1.5, you can use the special "new" template string to generate a public constructor instead of a factory method. This is experimental functionality.

@Value.Style(builder = "new")
@Value.Immutable
interface Item {
  String name();
}

// Generated usage - public constructor:
ImmutableItem item = new ImmutableItem.Builder()
  .name("Apple")
  .build();

Important limitations of “new” constructor approach:

  • Does not work with @Value.Check (validation methods)
  • Does not work with @Value.Immutable(singleton = true)
  • Does not work with certain other functionality that requires indirection
  • Compile errors will be raised if these incompatibilities are detected

Why constructor approach exists: Some developers prefer the explicitness of new for object creation, and it can be marginally more efficient as it avoids an extra method call. However, factory methods provide more flexibility for future changes (e.g., pooling, caching).

Template usage: The asterisk (*) can be used to insert the type name:

@Value.Style(builder = "create*Builder")  // Creates createPersonBuilder()

Relationship to other styles:

  • This controls builder instantiation from immutable implementation classes
  • For top-level builders (factories, external builders), see newBuilder
  • The builder class name is controlled by typeBuilder

Example (value-fixture/src/org/immutables/fixture/style/HaveBuilderNew.java:10):

@Value.Style(builder = "new")  // Constructor instead of factory
@Value.Immutable
interface HaveBuilderNew {
  int value();
}

// Usage:
ImmutableHaveBuilderNew instance = new ImmutableHaveBuilderNew.Builder()
  .value(42)
  .build();

Note: The public constructor functionality is experimental and may have limitations or be adjusted in future versions. Test thoroughly if using this feature.

See also: newBuilder, build, typeBuilder, typeInnerBuilder


builderToString

Default: false

Description: When enabled, generates a toString() method on builders that is safe to call at any time during the building process, showing the current state of attribute initialization.

Historically, Immutables has aimed to keep generated code compact with only essential methods. Builder toString() methods were not generated by default. However, for debugging and logging purposes, it’s often useful to inspect the state of a builder before calling build().

When enabled, a toString() method is generated that:

  • Lists all attributes and their current values
  • Indicates which mandatory attributes are not yet set (e.g., <not set>)
  • Is safe to call even on incomplete builders (won’t throw exceptions)
  • Provides readable output for debugging

When disabled (default):

  • No toString() method is generated on builders
  • Keeps generated code more compact
  • Calling toString() on a builder uses the default Object.toString() (useless for debugging)
@Value.Style(builderToString = true)
@Value.Immutable
interface Person {
  String name();
  int age();
  Optional<String> nickname();
}

// Usage:
ImmutablePerson.Builder builder = ImmutablePerson.builder()
  .name("John");

System.out.println(builder);
// Output example: Person.Builder{name=John, age=<not set>, nickname=<not set>}

builder.age(30);
System.out.println(builder);
// Output example: Person.Builder{name=John, age=30, nickname=<not set>}

Use cases:

  • Debugging complex builder initialization logic
  • Logging builder state at various points
  • Testing and assertions in unit tests
  • Development and troubleshooting

Output format: The exact format may vary but typically includes:

  • Builder class name
  • All attributes with their current values
  • Special markers (like <not set>) for uninitialized mandatory attributes
  • Collection sizes for collection attributes

Performance consideration: Generating toString() methods adds a small amount of code to each builder class. For applications with many immutable types, this can add up. However, the runtime performance impact is negligible unless toString() is called frequently.

Recommendation: Enable this during development and debugging, but consider disabling it in production builds if code size is a concern. Alternatively, enable it globally via a style annotation and leave it on—the convenience often outweighs the minimal code size increase.

See also: builder, delegateToString, typeBuilder


builderVisibility

Default: BuilderVisibility.PUBLIC

Description: Controls the visibility (access level) of generated builder classes.

This style attribute allows you to restrict or control access to builders independently of the immutable implementation visibility. It’s particularly useful when you want to control how objects can be constructed while keeping implementations hidden.

Available options:

  • PUBLIC - Builder is always public (default)
  • SAME - Builder has the same visibility as the abstract value type
  • PACKAGE - Builder is package-private
@Value.Style(
  builderVisibility = Value.Style.BuilderVisibility.PACKAGE
)
@Value.Immutable
public interface Config {
  String value();
}

// Builder is package-private, only accessible within same package
// ImmutableConfig.Builder is package-private
// But Config interface itself is public

Visibility combinations: This works independently of visibility (implementation visibility), allowing flexible access control:

// Example: Public interface, private implementation, package builder
@Value.Style(
  visibility = Value.Style.ImplementationVisibility.PRIVATE,
  builderVisibility = Value.Style.BuilderVisibility.PACKAGE
)
@Value.Immutable
public interface ApiModel {
  String data();
}

// Usage within same package:
// - ApiModel is public (interface)
// - ImmutableApiModel is private (implementation)
// - ImmutableApiModel.Builder is package-private (builder)

Use cases:

  • PACKAGE builders: Restrict object construction to same package, useful for internal APIs
  • SAME visibility: Maintain consistent access control across type and builder
  • PUBLIC (default): Maximum accessibility, standard for public APIs

Interaction with other features:

Related Issues:

  • #1096: Jackson properties not copied to builder with PRIVATE implementation visibility
  • #1108: Request to externalize builder class separately from implementation
  • #1080: Cannot inherit from generated factory builder (final class issue)
  • #1431: Discussion about using Immutables for behavior-oriented classes
  • #1196: Builder.build() to return interface instead of abstract class

Example (value-fixture/src/org/immutables/fixture/style/NestingClassOrBuilder.java:24):

@Value.Style(
  implementationNestedInBuilder = true,
  builderVisibility = BuilderVisibility.PACKAGE
)

Alternative approach: To avoid potential javac warnings with enum-based configuration (see issue #291), use the string-based builderVisibilityString attribute instead.

See also: builderVisibilityString, visibility, overshadowImplementation, implementationNestedInBuilder


builderVisibilityString

Default: "" (empty, uses builderVisibility)

Description: String-based alternative to builderVisibility enum to avoid javac warnings when using enum values in annotations.

This attribute exists as a workaround for javac warnings mentioned in issue #291. When using enum values in style annotations, some compiler configurations may produce warnings. This string-based attribute provides the same functionality without triggering those warnings.

When specified: Overrides the enum-based builderVisibility setting.

Valid string values:

  • "PUBLIC" - Builder is always public
  • "SAME" - Builder has same visibility as abstract value type
  • "PACKAGE" - Builder is package-private
@Value.Style(builderVisibilityString = "PACKAGE")
@Value.Immutable
public interface Config {
  String value();
}

// Equivalent to:
// @Value.Style(builderVisibility = Value.Style.BuilderVisibility.PACKAGE)

Why this exists: Annotation processing and compilation can sometimes be sensitive to how enum constants are referenced in annotations. The string-based alternative sidesteps these issues while providing identical functionality.

Recommendation:

  • Use builderVisibility by default for type safety
  • Switch to builderVisibilityString if you encounter compiler warnings or issues
  • The string values are validated at annotation processing time, so typos will be caught

Note: This is purely a convenience/compatibility feature. The generated code is identical whether you use the enum or string version.

See also: builderVisibility, visibilityString


builtinContainerAttributes

Default: true

Description: Controls whether built-in convenience features are generated for automatically recognized container types such as Optional, List, Set, Map, etc.

By default, Immutables recognizes common container types and generates rich builder APIs for them:

  • Optional types: Unwrapped setters, Optional.of() handling
  • List/Set collections: add*(), addAll*() methods
  • Map types: put*(), putAll*() methods
  • Special handling for immutable collection construction

When set to false, this recognition is disabled. All attribute types are treated uniformly with only basic setters and getters, regardless of whether they’re containers or not.

When true (default):

@Value.Immutable
interface Data {
  Optional<String> value();
  List<Integer> numbers();
  Map<String, Integer> map();
}

// Rich builder API generated:
ImmutableData.builder()
  .value("unwrapped")              // Optional unwrapping
  .value(Optional.of("wrapped"))   // Also accepts Optional
  .addNumbers(1, 2, 3)             // Add single elements
  .addAllNumbers(List.of(4, 5))   // Add from iterable
  .putMap("key", 1)                // Put single entry
  .putAllMap(Map.of("a", 2))      // Put all from map
  .build();

When false:

@Value.Style(builtinContainerAttributes = false)
@Value.Immutable
interface Data {
  Optional<String> value();  // No special Optional handling
  List<Integer> numbers();   // No addNumbers(), addAllNumbers(), etc.
  Map<String, String> map(); // No putMap(), putAllMap(), etc.
}

// Only basic setters generated:
ImmutableData.builder()
  .value(Optional.of("text"))      // Must provide Optional
  .numbers(List.of(1, 2, 3))       // Must provide full List
  .map(Map.of("key", "value"))     // Must provide full Map
  .build();

Why disable this?: This provides a clean slate when you want fine-grained control over type generation:

  • You’re using custom encodings (type customizers) and don’t want built-in handling to interfere
  • You have domain-specific container types that shouldn’t get special treatment
  • You want consistent, predictable behavior across all attribute types
  • You’re migrating from other immutable libraries with different conventions

Important: This controls attribute type recognition (how container types are handled), not attribute kinds (like @Value.Default, @Nullable, @Value.Derived). Those annotations work independently of this setting.

Example (value-fixture/src/org/immutables/fixture/style/NoBuiltinContainers.java:22):

@Value.Style(builtinContainerAttributes = false)
@Value.Immutable
interface NoContainersStyle {
  List<String> items();
  // Only items(List<String>) setter generated
  // No addItems(), addAllItems() methods
}

Relationship to encodings: When disabled, registered encodings (via type customizers/annotation processors) will still be processed. This setting only affects the built-in, hard-coded container recognition logic.

See also: add, addAll, put, putAll


canBuild

Default: "" (empty, method not generated)

Description: Naming template for a builder method that returns a boolean indicating whether all mandatory attributes have been set and calling build() would succeed without throwing an exception.

By default, this method is not generated. To enable it, provide a non-empty naming template. The empty string both disables generation and serves as the default.

Feature flag behavior: Non-empty template enables generation AND names the method. The asterisk (*) can be used to insert the type name.

How it works: The generated method checks if all required (mandatory) attributes have been initialized:

  • Returns true if all mandatory attributes are set and build() would succeed
  • Returns false if any mandatory attributes are missing and build() would throw an exception
  • Does not validate business logic or @Value.Check constraints, only attribute presence
@Value.Style(canBuild = "canBuild")
@Value.Immutable
interface Config {
  String host();              // Mandatory
  int port();                 // Mandatory
  Optional<String> username(); // Optional - not checked
  @Value.Default
  boolean ssl() { return true; } // Has default - not checked
}

// Generated usage:
ImmutableConfig.Builder builder = ImmutableConfig.builder();

if (builder.canBuild()) {  // false - host and port not set
  Config config = builder.build(); // Would throw if called
}

builder.host("localhost");
if (builder.canBuild()) {  // still false - port not set
  builder.build();
}

builder.port(8080);
if (builder.canBuild()) {  // true - all mandatory attributes set
  Config config = builder.build(); // Safe to call
}

Using type name in template:

@Value.Style(canBuild = "isBuilder*Ready")
@Value.Immutable
interface Person {
  String name();
}

// Generates: boolean isBuilderPersonReady()

Use cases:

  • Conditional building logic where you may or may not have all required data
  • Form validation UI where you want to enable/disable submit buttons
  • Progressive building scenarios where data arrives asynchronously
  • Testing and debugging builder state

Related Issues:

  • #755: Original feature request to add method to check if builder can build safely
  • #852: Discussion about exposing isInitialized vs canBuild on builders
  • #802: Example usage with canBuild in nested interfaces

Example (value-fixture/src/org/immutables/fixture/style/CanBuild.java:20):

@Value.Style(canBuild = "isBuilder*Ready")  // Type name inserted at *
@Value.Immutable
interface CanBuild {
  int value();
}

// Usage:
ImmutableCanBuild.Builder builder = ImmutableCanBuild.builder();
assert !builder.isBuilderCanBuildReady(); // false
builder.value(42);
assert builder.isBuilderCanBuildReady();  // true

Important limitation: When using encodings (custom type handling), canBuild() might give incorrect answers. This is because building of attributes is delegated to encoding-specific construction routines, and the general builder infrastructure may not know if attributes can be safely constructed. The method only checks if raw attribute values have been set, not whether encoding-specific validation would pass.

Note: This is different from isInitialized which checks the initialization state on modifiable objects, not builders.

See also: build, buildOrThrow, isInitialized, validationMethod


clear

Default: "clear"

Description: Naming template for methods that clear all collection attributes and unset optional/nullable attributes in modifiable implementations.

This style attribute controls the naming of the “reset to initial state” method generated on @Value.Modifiable companion types. When called, this method:

  • Clears all collection attributes (Lists, Sets, Maps) to empty state
  • Unsets optional and nullable attributes
  • Resets the modifiable object to its initial, just-created state
  • Does NOT affect mandatory non-collection attributes (they remain set)

Only applies to: Modifiable companion types generated with @Value.Modifiable, not to builders or immutable implementations.

@Value.Style(clear = "reset")
@Value.Immutable
@Value.Modifiable
interface Data {
  List<String> items();
  Map<String, Integer> counts();
  Optional<String> description();
  String name(); // Mandatory - not cleared
}

// Generated modifiable usage:
ModifiableData data = ModifiableData.create()
  .addItems("a", "b")
  .putCounts("x", 1)
  .setDescription("desc")
  .setName("important");

data.reset();  // Clears items, counts, and description

// After reset:
// items() returns empty list
// counts() returns empty map
// description() returns Optional.empty()
// name() still returns "important" (mandatory attribute)

Method behavior:

  • Collection attributes: Cleared to empty (not set to null)
  • Optional attributes: Reset to Optional.empty()
  • Nullable attributes: Set to null
  • Mandatory non-collection attributes: Unchanged

Common naming choices:

  • "clear" (default) - Standard clearing terminology
  • "reset" - Emphasizes returning to initial state
  • "resetAll" - More explicit about scope
  • "clearAll" - Clear everything that can be cleared

Use case: Modifiable objects are often reused in loops or repeated operations. The clear method allows efficient reuse without reallocating:

ModifiableData data = ModifiableData.create();
for (Record record : records) {
  data.reset();  // Clear from previous iteration
  data.setName(record.name());
  data.addItems(record.items());
  process(data.toImmutable());
}

Note: For builders, see clearBuilder which provides similar functionality but for builder instances.

See also: clearBuilder, unset, typeModifiable, @Value.Modifiable


clearBuilder

Default: false

When enabled, generates a clear() method on builder to reset its state. Primarily designed for resource-constrained environments to minimize allocations by reusing builder instances.

Note: This is disabled by default because creating fresh builders with a clean state is usually better—in server-side Java it may be more efficient to allocate a new builder than clean a previously allocated one.

Usage:

@Value.Style(clearBuilder = true)
@Value.Immutable
interface Point {
  int x();
  int y();
}

// Generated usage - builder reuse:
ImmutablePoint.Builder builder = ImmutablePoint.builder();
ImmutablePoint p1 = builder.x(1).y(2).build();
builder.clear();  // Reset to initial state
ImmutablePoint p2 = builder.x(3).y(4).build();

Performance trade-offs:

  • Allocation saving: Reusing builders reduces object allocation in tight loops
  • Server-side Java: Usually more efficient to create fresh builders
  • Mobile/resource-constrained: Can be beneficial when GC pressure is high
  • Microbenchmarks: May show slowdown due to clearing overhead

Interaction with deferCollectionAllocation: When both clearBuilder and deferCollectionAllocation are enabled:

  • If collections were never allocated: clear() is a no-op
  • If collections were allocated: Collections are cleared (not set to null) to preserve allocation

Related Issues:

  • #329: Discusses deferring collection allocation and clearing builders - collections cleared vs set to null
  • #138: Builder re-use discussion and clear method implementation

See also: clear, deferCollectionAllocation, strictBuilder


copyOf

Default: "copyOf"

Naming template for static method that creates an immutable copy from an instance of the abstract value type or implementation.

Method behavior:

  • Accepts any instance of the abstract value type
  • Returns an immutable implementation instance
  • If instance is already the immutable implementation, returns it as-is (no copy)
  • Otherwise, extracts all attribute values and constructs new immutable instance

Usage:

@Value.Style(copyOf = "immutableCopyOf")
@Value.Immutable
interface Item {
  String name();
  int quantity();
}

// Generated usage:
Item item = ...; // Some instance (could be custom implementation)
Item immutable = ImmutableItem.immutableCopyOf(item);

// No copy if already immutable:
ImmutableItem original = ImmutableItem.builder().name("x").quantity(1).build();
ImmutableItem copy = ImmutableItem.immutableCopyOf(original);
assert original == copy; // Same reference!

Deep copying behavior: When deepImmutablesDetection is enabled, copyOf will recursively copy nested immutable value objects:

@Value.Style(deepImmutablesDetection = true)
@Value.Immutable
interface Address {
  String street();
}

@Value.Immutable
interface Person {
  Address address(); // Nested immutable
}

// Deep copy - Address also converted to ImmutableAddress
Person person = ImmutablePerson.copyOf(customPersonImpl);

Disabling copyOf: Set @Value.Immutable(copy = false) to disable generation of copyOf method entirely.

Related Issues:

  • #1141: Use builder for copyOf to avoid multiple copies when using withers
  • #1618: Staged Builders, Withers, CopyOf() for RecordBuilders
  • #1586: Bug with mutables in copy - mutable collections not cloned
  • #700: Copy of primitive array with @Nullable - arrays not properly cloned
  • #1321: Recursive copy-of methods for deeply nested structures

See also: from, toImmutable, with, @Value.Immutable(copy = ...)


create

Default: "create"

Naming template for factory method of modifiable companion implementations. Can be set to "new" to use constructor instead.

Usage with factory method:

@Value.Style(create = "newInstance")
@Value.Immutable
@Value.Modifiable
interface Config {
  String value();
}

// Generated factory method:
ModifiableConfig config = ModifiableConfig.newInstance();
config.setValue("test");

Usage with constructor (special “new” token):

@Value.Style(create = "new")
@Value.Immutable
@Value.Modifiable
interface Config {
  String value();
}

// Uses public constructor:
ModifiableConfig config = new ModifiableConfig();
config.setValue("test");

Applies only to:

  • @Value.Modifiable companion classes
  • Does NOT affect immutable implementations
  • Immutable implementations use of for factory methods

When to use “new” vs factory method:

  • Factory method (default): More flexible, can return cached instances or subclasses
  • Constructor: More familiar Java pattern, direct instantiation

See also: typeModifiable, of, builder, @Value.Modifiable


defaultAsDefault

Default: false

When enabled, default accessor methods defined in interfaces automatically behave as if annotated with @Value.Default, without requiring explicit annotation.

Note: This is not enabled by default to preserve compatibility and to allow projects to opt-in only when needed.

Usage:

@Value.Style(defaultAsDefault = true)
@Value.Immutable
interface Record {
  long id();
  String name();

  default String notes() {  // Automatically treated as @Value.Default
    return "";
  }

  default int version() {  // Also becomes @Value.Default
    return 1;
  }
}

// Generated usage:
Record record = ImmutableRecord.builder()
  .id(123L)
  .name("Named")
  // notes() and version() not required, use defaults
  .build();

assert record.notes().equals("");
assert record.version() == 1;

Without this style (explicit annotation required):

@Value.Immutable
interface Record {
  long id();

  @Value.Default  // Must explicitly annotate
  default String notes() {
    return "";
  }
}

Why not enabled by default:

  • Backward compatibility: Existing codebases may have default methods intended as non-attribute helpers
  • Explicit opt-in: Makes it clear that default methods become part of the value object contract
  • Flexibility: Allows mixing default helper methods with default attributes

Example (value-fixture/src/org/immutables/fixture/style/Conservative.java:30):

@Value.Style(
    defaultAsDefault = true,
    get = {"get*", "is*"}
)
@interface Conservative {}

Common use case: Works well with Java 8+ interfaces where you want clean, annotation-free default values:

@Value.Style(defaultAsDefault = true)
@Value.Immutable
interface ServerConfig {
  String host();

  default int port() { return 8080; }
  default int timeout() { return 30000; }
  default boolean ssl() { return false; }
}

See also: @Value.Default, @Value.Derived


defaults

Default: @Value.Immutable (with default attribute values)

Specifies default @Value.Immutable attribute values for all immutable objects using this style. These defaults apply only if no @Value.Immutable attributes are specified inline.

Usage - Style meta-annotation:

@Value.Style(
  defaults = @Value.Immutable(
    copy = false,      // Disable withX() methods by default
    builder = true,    // Enable builders by default
    intern = false,    // Don't intern instances
    singleton = false  // Don't generate singletons
  )
)
public @interface MyStyle {}

@MyStyle
@Value.Immutable  // Uses ALL defaults from MyStyle
interface Simple {
  String value();
}

@MyStyle
@Value.Immutable(copy = true)  // Specifies one attribute - ALL defaults ignored!
interface WithCopy {
  String value();
  // builder will be TRUE (global default), not from style defaults!
}

Critical behavior - All-or-nothing: If any @Value.Immutable attribute is specified inline, the defaults are completely ignored:

@Value.Style(
  defaults = @Value.Immutable(
    copy = false,
    builder = false,
    intern = true
  )
)
public @interface AllOrNothing {}

@AllOrNothing
@Value.Immutable
// ✓ Uses style defaults: copy=false, builder=false, intern=true
interface A {
  String value();
}

@AllOrNothing
@Value.Immutable(copy = true)
// ✗ Style defaults IGNORED! Uses global Immutable defaults
// copy=true (specified), builder=TRUE (global), intern=FALSE (global)
interface B {
  String value();
}

Example (value-fixture/src/org/immutables/fixture/style/Tuple.java:21): Tuple-style values with constructor parameters, no builder:

@Value.Style(
  typeImmutable = "*Tuple",
  allParameters = true,
  defaults = @Value.Immutable(
    builder = false,  // No builder for tuples
    copy = false      // No withX() for tuples
  )
)
public @interface Tuple {}

@Tuple
@Value.Immutable
interface Point {
  int x();
  int y();
}

// Usage: constructor-style
PointTuple point = PointTuple.of(10, 20);

Common patterns:

// Pattern 1: Minimal immutables (no builder, no copy)
@Value.Style(defaults = @Value.Immutable(builder = false, copy = false))
public @interface Minimal {}

// Pattern 2: Data class style (builder, no copy, lazy hash)
@Value.Style(defaults = @Value.Immutable(lazyhash = true, copy = false))
public @interface DataClass {}

// Pattern 3: Cached singletons
@Value.Style(defaults = @Value.Immutable(singleton = true, intern = true))
public @interface Cached {}

Available defaults (from @Value.Immutable):

  • singleton - Generate singleton instance
  • intern - Intern instances
  • copy - Generate copyOf and withX methods
  • prehash - Precompute hashCode in constructor
  • lazyhash - Compute hashCode lazily
  • builder - Generate builder

See also: @Value.Immutable, meta-annotations


deferCollectionAllocation

Default: false

When enabled, defers allocation of collection/map builders until the first element is added. Collections start as null and are lazily initialized on first access, potentially saving allocations for unused attributes.

Note: The resulting code might be somewhat slower at a microbenchmark scale due to additional “if” checks. Does not work when strictBuilder is enabled. Disabled by default.

Generated code difference:

Without deferCollectionAllocation (default):

public static class Builder {
  private List<String> items = new ArrayList<>();  // Allocated immediately

  public Builder addItem(String item) {
    this.items.add(item);
    return this;
  }
}

With deferCollectionAllocation:

public static class Builder {
  private @Nullable List<String> items;  // Starts null

  public Builder addItem(String item) {
    if (this.items == null) {  // Lazy initialization
      this.items = new ArrayList<>();
    }
    this.items.add(item);
    return this;
  }
}

Usage:

@Value.Style(deferCollectionAllocation = true)
@Value.Immutable
interface Report {
  String title();
  List<String> tags();     // May not be used
  List<String> warnings(); // May not be used
  Map<String, String> metadata(); // May not be used
}

// If tags/warnings/metadata never populated, no List/Map allocated
Report report = ImmutableReport.builder()
    .title("Summary")
    .build();  // Only title populated, collections never allocated

Performance trade-offs:

  • Saves allocations: When many collection attributes exist but few are used
  • Adds overhead: Extra null checks on every collection operation
  • Microbenchmarks: May show slowdown due to branch prediction
  • Real-world: Can help in object-heavy scenarios with sparse collections

Interaction with clearBuilder: When both enabled, clear() method clears existing collections rather than setting to null (preserves allocation):

if (this.items != null) {
  this.items.clear();  // Reuse ArrayList
}

Restrictions:

  • Does NOT work with strictBuilder = true: Strict builders pre-allocate collections
  • Only affects builders and modifiables: Immutable instances always have collections

Related Issues:

  • #329: Original discussion on deferring collection allocation and clearing behavior
  • #307: Request for lazy collection initialization to save allocations

When to use:

  • ✓ Many collection attributes, most remain empty
  • ✓ Resource-constrained environments (mobile, embedded)
  • ✓ Large object graphs with sparse collections
  • ✗ Hot path builders (microbenchmark overhead)
  • ✗ Used with strictBuilder (incompatible)

See also: strictBuilder, clearBuilder


delegateToString

Default: "" (empty, not used)

Fully qualified path to a static method to which toString() implementation is completely delegated. The path will be used literally in generated code, and the this immutable object instance will be passed as a single parameter to it.

Note: When specified, takes absolute precedence over all other toString customization mechanisms.

Usage:

package com.example;

public class ToStringUtils {
  // Must be static, accept Object or specific type, return String
  public static String stringify(Object obj) {
    return "Custom[" + obj.getClass().getSimpleName() + "@" +
           System.identityHashCode(obj) + "]";
  }
}
@Value.Style(delegateToString = "com.example.ToStringUtils.stringify")
@Value.Immutable
interface Item {
  String name();
  int quantity();
}

// Generated toString() implementation:
@Override
public String toString() {
  return com.example.ToStringUtils.stringify(this);
}

Method signature requirements:

// Option 1: Accept Object
public static String stringify(Object obj) { ... }

// Option 2: Accept specific type
public static String stringify(Item item) { ... }

// Option 3: Type parameter
public static <T> String stringify(T obj) { ... }

Precedence - delegateToString overrides:

  1. underrideToString - Custom toString in abstract class
  2. limitStringLengthInToString - String length limiting
  3. Default generated toString
  4. Any other toString customization

Use cases:

1. Consistent formatting across project:

@Value.Style(delegateToString = "com.acme.Formatters.toJson")
public @interface AcmeStyle {}

@AcmeStyle
@Value.Immutable
interface User {
  String name();
}

// All types use JSON toString
System.out.println(user);  // {"name":"Alice"}

2. Performance-sensitive toString:

public class FastToString {
  public static String stringify(Object obj) {
    // Custom fast implementation, maybe using StringBuilder pool
    return fastFormat(obj);
  }
}

3. Debug-friendly toString:

public class DebugToString {
  public static String stringify(Object obj) {
    return String.format("%s@%08x",
        obj.getClass().getSimpleName(),
        System.identityHashCode(obj));
  }
}

@Value.Style(delegateToString = "com.debug.DebugToString.stringify")

Important notes:

  • The method path is used literally in generated code (no validation at compile time)
  • Method must be accessible from the generated class’s package
  • No automatic imports - use fully qualified name
  • The delegate method receives this (the immutable instance)

Generated code example:

@Override
public String toString() {
  return com.example.ToStringUtils.stringify(this);
}

See also: underrideToString, limitStringLengthInToString, includeHashCode


depluralize

Default: false

Enables automatic depluralization (singularization) of collection and map attribute names for generating derived method names like add, addAll, and put. Exceptions to mechanical depluralization rules can be provided using depluralizeDictionary array of “singular:plural” pairs.

Mechanical depluralization rules:

  1. Trailing “s”: boatsboat, dogsdog
  2. “ies” to “y”: entriesentry, categoriescategory
  3. Dictionary exceptions: See depluralizeDictionary
  4. Last segment only: feetPeoplefeetPerson (only “People” → “Person”)

Usage:

@Value.Style(
  depluralize = true,
  depluralizeDictionary = {"person:people", "foot:feet"}
)
@Value.Immutable
interface Team {
  List<String> people();    // → addPerson(), not addPeople()
  List<String> feet();      // → addFoot(), not addFeet()
  List<String> boats();     // → addBoat() (mechanical)
  List<String> entries();   // → addEntry() ("ies" → "y")
  List<String> data();      // → addData() (no change)
}

// Generated builder API:
Team team = ImmutableTeam.builder()
    .addPerson("Alice")      // singular
    .addPerson("Bob")
    .addFoot("left")         // singular
    .addFoot("right")
    .addBoat("Titanic")      // singular
    .addEntry("key=value")   // singular
    .build();

Depluralization examples:

Attribute NameMechanical ResultWith DictionaryGenerated Method
boatsboataddBoat()
peoplepeople (no change)personaddPerson()
feetfeet (no change)footaddFoot()
entriesentry (“ies”→“y”)addEntry()
categoriescategoryaddCategory()
feetPeoplefeetPersonfoot:feet, person:peopleaddFeetPerson()
peopleRepublicspeopleRepublicperson:peopleaddPeopleRepublic()
datadata (no change)addData()
goodsgoods (if in dict)“goods” (suppress)addGoods()

Camel-case handling: Only the last segment of a camelCase identifier is considered:

  • feetPeople → last segment is “People” → depluralized to “Person” → feetPerson
  • activeUsers → last segment is “Users” → depluralized to “User” → activeUser

Affected methods:

  • add: addPerson() instead of addPeople()
  • addAll: addAllPeople() (plural preserved for “all”)
  • put: For maps with pluralized keys

Example (value-fixture/src/org/immutables/fixture/style/Depluralize.java:22):

@Value.Style(
  depluralize = true,
  depluralizeDictionary = {"foot:feet", "person:people", "goods"}
)
public @interface Depluralized {}

@Depluralized
@Value.Immutable
interface Organization {
  List<String> people();
  Set<String> categories();
}

// Usage:
Organization org = ImmutableOrganization.builder()
    .addPerson("Alice")     // singular!
    .addCategory("Tech")    // "ies" → "y"
    .build();

Why use depluralization:

  • Natural English: addPerson() is more natural than addPeople()
  • Consistency: Aligns with Java conventions (e.g., List.add vs List.addAll)
  • Domain modeling: Better matches ubiquitous language in DDD

Dictionary-based depluralization: Handles both:

See also: depluralizeDictionary, add, addAll, put, @Value.Style.Depluralize


depluralizeDictionary

Default: {} (empty array)

Array of “singular:plural” pairs defining exceptions to mechanical depluralization. Used with depluralize to handle irregular plural forms that don’t follow simple “s” or “ies→y” rules. Suppress trimming of trailing “s” for certain words by using short form "words" (equivalent to "words:words"). Words will be converted to lowercase and for identifiers consisting of one or more words joined using camel case, only the last segment will be considered for depluralization when matching dictionary. Uninterpretable pairs will be ignored. By default, no dictionary is supplied and depluralization is performed only by mechanical “*s” trimming.

Note: This attribute is semi-deprecated in favor of using @Value.Style.Depluralize#dictionary() annotation which may be placed on a package, type, or as meta-annotation. Full dictionary will be merged across all applicable definitions.

Format:

depluralizeDictionary = {
  "singular:plural",  // Standard form
  "singular:plural",
  "word"              // Short form (equivalent to "word:word")
}

Usage:

@Value.Style(
  depluralize = true,
  depluralizeDictionary = {
    "person:people",      // people → person
    "child:children",     // children → child
    "mouse:mice",         // mice → mouse
    "goose:geese",        // geese → goose
    "data:data",          // data → data (suppress transformation)
    "sheep:sheep",        // sheep → sheep (suppress)
    "goods"               // Short form: goods → goods (suppress)
  }
)
@Value.Immutable
interface Report {
  List<Person> people();     // → addPerson()
  List<Child> children();    // → addChild()
  List<Mouse> mice();        // → addMouse()
  List<String> data();       // → addData() (not addDatum)
  List<Item> goods();        // → addGoods() (not addGood)
}

Dictionary matching rules:

  1. Case-insensitive: “Person:People” matches “people”, “PEOPLE”, “People”
  2. Last segment only: For feetPeople, only “People” is checked against dictionary
  3. Lowercase comparison: All comparisons done in lowercase
  4. Exact match required: Partial matches don’t work

Depluralization examples with dictionary:

AttributeDictionary EntryMechanicalFinal ResultMethod Generated
boatsboatboataddBoat()
peopleperson:peoplepeoplepersonaddPerson()
childrenchild:childrenchildrenchildaddChild()
feetfoot:feetfeetfootaddFoot()
micemouse:micemicemouseaddMouse()
datadata:datadatadataaddData()
goodsgoodsgoodgoodsaddGoods()
feetPeoplefoot:feet, person:peoplefeetPeoplefeetPersonaddFeetPerson()
categoriescategorycategoryaddCategory()

Short form syntax:

"goods"  // Equivalent to "goods:goods"

Suppresses depluralization: goodsgoods (not good)

Camel-case handling: Dictionary matching applies to last segment only:

depluralizeDictionary = {"person:people", "foot:feet"}

// Attribute: feetPeople
// Last segment: "People" → matches "person:people" → becomes "Person"
// Result: feetPerson → addFeetPerson()

// Attribute: peopleManagers
// Last segment: "Managers" → mechanical "*s" trim → becomes "Manager"
// Result: peopleManager → addPeopleManager()

Common dictionaries:

English irregular plurals:

depluralizeDictionary = {
  "person:people",
  "child:children",
  "man:men",
  "woman:women",
  "tooth:teeth",
  "foot:feet",
  "mouse:mice",
  "goose:geese"
}

Suppression (words that shouldn’t change):

depluralizeDictionary = {
  "data:data",      // Not "datum"
  "sheep:sheep",
  "fish:fish",
  "series:series",
  "species:species",
  "goods",          // Short form
  "news"
}

Domain-specific:

depluralizeDictionary = {
  "index:indices",    // Technical: indices, not indexes
  "vertex:vertices",  // Math: vertices
  "axis:axes",        // Geometry: axes
  "analysis:analyses" // Research: analyses
}

Semi-deprecated - Prefer @Value.Style.Depluralize: The newer annotation approach is more modular:

@Value.Style.Depluralize(dictionary = {
  "person:people",
  "child:children"
})
package com.example.domain;

Dictionaries from multiple sources are merged:

  • Package-level @Value.Style.Depluralize
  • Type-level @Value.Style.Depluralize
  • Meta-annotation @Value.Style.Depluralize
  • Style depluralizeDictionary attribute

Example (value-fixture/src/org/immutables/fixture/style/Depluralize.java:22):

@Value.Style(
  depluralize = true,
  depluralizeDictionary = {
    "foot:feet",
    "person:people",
    "goods"  // Short form - suppress depluralization
  }
)
public @interface Depluralized {}

Best practices:

  • Use dictionary for irregular plurals (person/people, child/children)
  • Use dictionary to suppress changes for words that shouldn’t be depluralized (data, goods)
  • Keep dictionary focused - mechanical rules handle most cases
  • Consider domain ubiquitous language when choosing singular forms

See also: depluralize, @Value.Style.Depluralize, add


deepImmutablesDetection

Default: false

Enables deep analysis of immutable types for additional convenience features. When enabled, attributes that are themselves @Value.Immutable types receive special handling with two key features:

1. Covariant return types

Accessors return the concrete immutable implementation type instead of the abstract type (applies only to non-collection attributes):

@Value.Style(deepImmutablesDetection = true)
public interface Canvas {
  @Value.Immutable
  interface Color {
    @Value.Parameter double red();
    @Value.Parameter double green();
    @Value.Parameter double blue();
  }

  @Value.Immutable
  interface Line {
    Color color();  // Declared as Color
  }
}

// Covariant return types allow assignment without casting:
ImmutableLine line = ...;
ImmutableColor color = line.color();  // Returns ImmutableColor, not Color!

Limitations for covariant returns:

  • Does NOT apply to collection/container attributes (to avoid issues with invariant generic types)
  • Does NOT apply to @Value.Derived or @Value.Default attributes (to avoid excessive complexity)

2. Builder shortcut initializers

Builder gets overloaded methods accepting constructor parameters of nested immutables, allowing more concise initialization:

@Value.Style(deepImmutablesDetection = true)
public interface Canvas {
  @Value.Immutable
  interface Point {
    @Value.Parameter int x();
    @Value.Parameter int y();
  }

  @Value.Immutable
  interface Line {
    Point start();
    Point end();
  }
}

// Shortcut initializers:
ImmutableLine line = ImmutableLine.builder()
  .start(1, 2)  // Equivalent to .start(ImmutablePoint.of(1, 2))
  .end(3, 4)    // Equivalent to .end(ImmutablePoint.of(3, 4))
  .build();

// Traditional way still works:
ImmutableLine line2 = ImmutableLine.builder()
  .start(ImmutablePoint.of(1, 2))
  .end(ImmutablePoint.of(3, 4))
  .build();

Shortcut initializers work for:

  • Regular attributes with @Value.Parameter constructor
  • Collection attributes (e.g., List<Point>addPoints(int x, int y))

Shortcut initializers DON’T work for:

  • Map attributes (to avoid confusing overloads)
  • Array attributes (to avoid confusing overloads)
  • Nested immutables that only have builders (no constructor)

Note: As of version 2.2, the *Of suffix is no longer added to shortcut initializer attributes.

Complete example from Canvas fixture

@Value.Style(deepImmutablesDetection = true)
public interface Canvas {
  @Value.Immutable
  interface Color {
    @Value.Parameter double red();
    @Value.Parameter double green();
    @Value.Parameter double blue();
  }

  @Value.Immutable
  interface Point {
    @Value.Parameter int x();
    @Value.Parameter int y();
  }

  @Value.Immutable
  interface Line {
    Color color();
    Point start();
    Point end();
  }
}

// Usage combining both features:
ImmutableLine line = ImmutableLine.builder()
    .start(1, 2)           // Shortcut for .start(ImmutablePoint.of(1, 2))
    .end(2, 3)             // Shortcut for .end(ImmutablePoint.of(2, 3))
    .color(0.9, 0.7, 0.4)  // Shortcut for .color(ImmutableColor.of(...))
    .build();

// Covariant return types:
ImmutablePoint start = line.start();  // Returns ImmutablePoint, not Point
ImmutablePoint end = line.end();      // Returns ImmutablePoint, not Point
ImmutableColor color = line.color();  // Returns ImmutableColor, not Color

Performance and processing considerations:

  • Disabled by default: May increase annotation processing time
  • Shallow analysis only: Works only with types discoverable during current processing round
  • Not for yet-to-be-generated types: If nested immutable is being generated in the same round, deep detection may not work
  • Compilation order matters: Generate nested immutables before types that reference them for best results

Related Issues:

  • #1239: Deep immutables detection with inheritance
  • #1375: Deep immutable detection with typed generics
  • #795: Recursive deep immutables detection
  • #684: Deep immutables detection with arrays
  • #794: Deep immutables detection with static constructors
  • #462: Deep immutables detection with wildcard types
  • #498: Visibility issues with deep immutables
  • #793: Deep immutables with staged builders
  • #1227: Improving robustness of deep detection
  • #1300: Bug with deep immutables and Optional
  • #1180: Missing annotations in deep immutables

Example (value-fixture/test/org/immutables/fixture/deep/DeepImmutablesTest.java demonstrates usage)

Version notes:

  • Before 2.2: Shortcut initializers had *Of suffix (e.g., startOf(1, 2))
  • From 2.2: Shortcut initializers use same name as regular initializers (e.g., start(1, 2))

See also: attributeBuilderDetection, copyOf, builtinContainerAttributes


fallbackNullableAnnotation

Default: java.lang.annotation.Inherited.class (placeholder meaning use javax.annotation.Nullable)

Specifies which nullable annotation to use when the processor needs to insert one internally without an “original” annotation to copy from. The specified annotation class must be on the compilation classpath (as it’s specified as a class literal in the style annotation). It will be used verbatim in all places where the processor needs to insert a nullable annotation without having an “original” annotation to copy.

When is this needed?

Immutables typically copies @Nullable annotations from your abstract value type to generated code. However, there are cases where the processor needs to insert a nullable annotation without having an original annotation to copy:

  1. TYPE_USE annotations: When using type-use nullable annotations (e.g., @Nullable String vs String @Nullable), they don’t always propagate automatically to generated signatures
  2. Generated method parameters: When the processor creates new method signatures that need nullable annotations
  3. Builder methods: When generating builder methods with optional parameters
  4. Internal generated code: When internal implementation details require nullable markers

Default behavior:

// By default (if fallbackNullableAnnotation not specified):
// Uses javax.annotation.Nullable if found on classpath

// Generated code might look like:
@javax.annotation.Nullable  // Automatically inserted
public String getOptionalValue() { ... }

Customizing the fallback annotation:

@Value.Style(
  fallbackNullableAnnotation = org.jetbrains.annotations.Nullable.class
)
@Value.Immutable
interface Config {
  Optional<String> optionalValue();
}

// Generated code will use JetBrains Nullable:
@org.jetbrains.annotations.Nullable
public String getOptionalValue() { ... }

Common nullable annotations:

// JSR-305 (most common):
@Value.Style(fallbackNullableAnnotation = javax.annotation.Nullable.class)

// JetBrains (IntelliJ IDEA):
@Value.Style(fallbackNullableAnnotation = org.jetbrains.annotations.Nullable.class)

// Eclipse JDT:
@Value.Style(fallbackNullableAnnotation = org.eclipse.jdt.annotation.Nullable.class)

// SpotBugs (successor to FindBugs):
@Value.Style(fallbackNullableAnnotation = edu.umd.cs.findbugs.annotations.Nullable.class)

// Checker Framework:
@Value.Style(fallbackNullableAnnotation = org.checkerframework.checker.nullness.qual.Nullable.class)

// Android Support:
@Value.Style(fallbackNullableAnnotation = androidx.annotation.Nullable.class)

// Jakarta (for Jakarta EE migration):
@Value.Style(fallbackNullableAnnotation = jakarta.annotation.Nullable.class)

Relationship with passAnnotations:

fallbackNullableAnnotation works together with passAnnotations to handle nullable annotations:

@Value.Style(
  passAnnotations = {org.jetbrains.annotations.Nullable.class},
  fallbackNullableAnnotation = org.jetbrains.annotations.Nullable.class
)
@Value.Immutable
interface Data {
  @org.jetbrains.annotations.Nullable  // Copied via passAnnotations
  String explicitNullable();

  Optional<String> optionalValue();    // Uses fallbackNullableAnnotation
}

Important notes:

  • Must be on compilation classpath: The annotation class must be available during compilation (as it’s specified as a class literal)
  • Not validated: The annotation is NOT checked for applicability or proper @Target settings
  • Used verbatim: The annotation is inserted exactly as specified in all places where needed
  • Placeholder default: The default value Inherited.class is just a placeholder—it tells Immutables to look for javax.annotation.Nullable on classpath

Example with TYPE_USE annotations:

import org.checkerframework.checker.nullness.qual.Nullable;

@Value.Style(
  fallbackNullableAnnotation = Nullable.class,
  passAnnotations = {Nullable.class}
)
@Value.Immutable
interface Data {
  @Nullable String value();  // TYPE_USE nullable annotation
}

// Generated code uses fallback for all internal nullable needs:
public @Nullable String value() { ... }

Interaction with Jakarta:

When using jakarta = true, the processor prefers Jakarta packages over javax. However, fallbackNullableAnnotation still needs to be explicitly set:

@Value.Style(
  jakarta = true,
  fallbackNullableAnnotation = jakarta.annotation.Nullable.class
)

See also: nullableAnnotation, passAnnotations, jakarta


finalInstanceFields

Default: true

Controls whether instance fields in the generated immutable implementation class are marked final.

Note: Disable final fields only if there is no other way — it is considered unsafe. This setting only affects instance fields of the immutable implementation class and will not apply to many other places/generators.

Default behavior (finalInstanceFields = true):

@Value.Immutable
interface Data {
  String value();
  int count();
}

// Generated implementation:
final class ImmutableData implements Data {
  private final String value;  // Final field
  private final int count;     // Final field

  ImmutableData(String value, int count) {
    this.value = value;
    this.count = count;
  }
}

With finalInstanceFields = false:

@Value.Style(finalInstanceFields = false)
@Value.Immutable
interface Data {
  String value();
  int count();
}

// Generated implementation:
final class ImmutableData implements Data {
  private String value;  // Non-final field!
  private int count;     // Non-final field!

  ImmutableData(String value, int count) {
    this.value = value;
    this.count = count;
  }
}

Why would you disable final fields?

There are very few legitimate reasons to disable final fields, but they include:

  1. Reflection-based frameworks: Some frameworks (like certain ORM or serialization libraries) require non-final fields to set values via reflection
  2. Framework-required default constructors: When combined with protectedNoargConstructor, allows frameworks to create instances and then populate fields
  3. Java serialization compatibility: Some legacy Java serialization mechanisms require mutable fields
  4. Proxy generation: Some dynamic proxy or bytecode manipulation frameworks may require non-final fields

Example with framework integration:

// For frameworks requiring reflection-based initialization:
@Value.Style(
  finalInstanceFields = false,          // Allow field mutation
  protectedNoargConstructor = true      // Provide no-arg constructor
)
@Value.Immutable
interface Entity {
  long id();
  String name();
}

// Generated code allows:
// 1. Creating instance via no-arg constructor
// 2. Setting field values via reflection

Example from fixtures (value-fixture/src/org/immutables/fixture/style/NonFinalInstanceFields.java:21):

@Value.Style(
  finalInstanceFields = false,
  protectedNoargConstructor = true
)

Critical warnings:

  • ⚠️ Breaks immutability guarantees: Non-final fields can be modified via reflection, breaking the immutability contract
  • ⚠️ Thread-safety concerns: Without final fields, proper publication of values is not guaranteed by the JMM (Java Memory Model)
  • ⚠️ Considered unsafe: This should be a last resort option only when no other approach works
  • ⚠️ Limited applicability: Only affects instance fields in the Immutable implementation class; doesn’t apply to all generated code

Memory model implications:

The Java Memory Model guarantees that final fields are properly published when an object is constructed. Without final fields:

// With final fields (safe):
final class ImmutableData {
  private final String value;  // Guaranteed visible to all threads after construction
}

// Without final fields (unsafe):
class ImmutableData {
  private String value;  // May not be visible to other threads without synchronization!
}

Alternatives to consider before disabling final fields:

  1. Use copyOf methods: Most frameworks can work with immutable objects if you provide proper factory methods
  2. Custom deserializers: Write custom Jackson/Gson deserializers that use the builder
  3. Adapter pattern: Create a mutable adapter class that converts to/from immutable objects
  4. Framework-specific encodings: Use Immutables’ encoding feature to integrate with specific frameworks

What IS still final even with finalInstanceFields = false:

  • The generated class itself remains final
  • Static fields remain final
  • Method parameters in generated code
  • Local variables in generated methods

What is affected:

  • Only the instance fields storing attribute values in the immutable implementation class

Performance considerations:

Final fields can enable JVM optimizations:

  • Constant folding: The JVM can optimize access to final fields
  • Escape analysis: Better escape analysis with final fields
  • Memory barriers: Final fields have specific memory barrier guarantees

Testing immutability with non-final fields:

If you must use non-final fields, ensure your tests verify immutability:

@Test
void testImmutability() {
  ImmutableData data = ImmutableData.builder()
    .value("test")
    .build();

  String original = data.value();

  // Attempt to mutate via reflection (should be detected in tests):
  Field field = ImmutableData.class.getDeclaredField("value");
  field.setAccessible(true);
  field.set(data, "changed");

  // Verify this doesn't happen in production code!
}

See also: protectedNoargConstructor, privateNoargConstructor


forceEqualsInWithers

Default: false

When enabled, generated with*() copy methods include equality checks and return the same instance if the value hasn’t changed, avoiding unnecessary object allocation. The processor uses == for integer primitives, floatToIntBits == for floating point, and .equals() for reference values.

Note: Some usage patterns might require strict equality checks during copy to function properly, while for other usages it’s just an optimization.

Default behavior (forceEqualsInWithers = false):

@Value.Immutable
interface Point {
  int x();
  int y();
}

// Generated withX always creates new instance:
public Point withX(int x) {
  return new ImmutablePoint(x, this.y);  // Always allocates
}

With forceEqualsInWithers = true:

@Value.Style(forceEqualsInWithers = true)
@Value.Immutable
interface Point {
  int x();
  int y();
}

// Generated withX checks equality first:
public Point withX(int x) {
  if (this.x == x) return this;  // Avoid allocation if value unchanged
  return new ImmutablePoint(x, this.y);
}

Equality checking strategies:

The processor uses different equality checks depending on the attribute type:

@Value.Style(forceEqualsInWithers = true)
@Value.Immutable
interface Data {
  // Integer primitives: ==
  int count();
  // → if (this.count == count) return this;

  // Long primitives: ==
  long id();
  // → if (this.id == id) return this;

  // Float: floatToIntBits
  float temperature();
  // → if (Float.floatToIntBits(this.temperature) == Float.floatToIntBits(temperature)) return this;

  // Double: doubleToLongBits
  double precision();
  // → if (Double.doubleToLongBits(this.precision) == Double.doubleToLongBits(precision)) return this;

  // Reference types: .equals()
  String name();
  // → if (this.name.equals(name)) return this;

  // Nullable reference types: null-safe equals
  @Nullable String optional();
  // → if (Objects.equals(this.optional, optional)) return this;
}

Benefits:

  1. Reduced allocations: Avoids creating new objects when values don’t change
  2. Identity preservation: Returns same instance, allowing == checks in some scenarios
  3. Cache-friendly: Can help with caching strategies that rely on object identity
  4. Performance optimization: Especially useful in update-heavy scenarios

When to enable:

// Scenario 1: Frequent updates with mostly unchanged values
@Value.Style(forceEqualsInWithers = true)
@Value.Immutable
interface GameState {
  int score();
  int level();
  String playerName();
}

GameState state = ...;
// Many updates but score often stays same:
for (Event event : events) {
  state = state.withScore(calculateScore(event));  // Often returns same instance
}

// Scenario 2: Usage patterns requiring reference equality
@Value.Style(forceEqualsInWithers = true)
@Value.Immutable
interface CachedValue {
  String key();
  Object value();
}

CachedValue cached = ...;
CachedValue updated = cached.withValue(newValue);
if (cached == updated) {
  // Value didn't change, no need to invalidate cache
}

// Scenario 3: Chained updates where intermediate values might be equal
person = person
  .withName(getName())      // Might not change
  .withAge(getAge())        // Might not change
  .withCity(getCity());     // Might not change
// Fewer allocations if values are often the same

When NOT to enable:

  1. Most updates change values: If values usually differ, equality checks add overhead without benefit
  2. Immutability purity: If you rely on each with*() call producing a new instance for semantics
  3. Debugging: New instances make it easier to track object creation in profilers

Performance comparison:

@Value.Style(forceEqualsInWithers = true)
@Value.Immutable
interface Point {
  int x();
  int y();
}

Point p = ImmutablePoint.builder().x(10).y(20).build();

// Without forceEqualsInWithers:
Point p2 = p.withX(10);  // Always allocates new ImmutablePoint

// With forceEqualsInWithers:
Point p3 = p.withX(10);  // Returns same instance (p == p3)
Point p4 = p.withX(15);  // Allocates new instance (p != p4)

Interaction with collection attributes:

For collection attributes, equality checks still apply but use collection’s .equals():

@Value.Style(forceEqualsInWithers = true)
@Value.Immutable
interface Data {
  List<String> items();
}

Data data = ...;
List<String> sameList = data.items();
Data updated = data.withItems(sameList);  // Returns same instance!
// Because sameList.equals(data.items()) is true

Example from fixtures (value-fixture/src/org/immutables/fixture/style/ForceEqualsInWithers.java:22):

@Value.Style(forceEqualsInWithers = true)

Null handling:

For nullable attributes, the processor uses null-safe equality:

@Value.Style(forceEqualsInWithers = true)
@Value.Immutable
interface Data {
  @Nullable String value();
}

// Generated code:
public Data withValue(@Nullable String value) {
  if (Objects.equals(this.value, value)) return this;  // Null-safe
  return new ImmutableData(value);
}

Related Issues:

  • #21: Original feature request for equality checks in wither methods

Trade-offs:

Pros:

  • Reduces object allocation in update-heavy code
  • Enables reference equality checks for optimization
  • Can improve cache hit rates

Cons:

  • Adds comparison overhead to every with*() call
  • May complicate reasoning about object identity
  • Minimal benefit if values usually change

Best practices:

  1. Enable project-wide via meta-annotation if your domain has many small updates:

    @Value.Style(forceEqualsInWithers = true)
    @Target({ElementType.PACKAGE, ElementType.TYPE})
    @Retention(RetentionPolicy.CLASS)
    public @interface MyStyle {}
  2. Profile before enabling: Measure allocation rates to see if this optimization helps

  3. Document behavior: Make it clear to API users that with*() may return same instance

See also: with, copyOf


forceJacksonIgnoreFields

Default: false

When true, adds @JsonIgnore annotations to all instance fields in generated immutable classes. This prevents Jackson from attempting to serialize/deserialize private fields directly.

Default behavior (forceJacksonIgnoreFields = false):

@Value.Immutable
interface Data {
  String name();
  int count();
}

// Generated class (without @JsonIgnore on fields):
class ImmutableData {
  private final String name;
  private final int count;
  // Jackson might access these fields directly via reflection
}

With forceJacksonIgnoreFields = true:

@Value.Style(forceJacksonIgnoreFields = true)
@Value.Immutable
interface Data {
  String name();
  int count();
}

// Generated class:
class ImmutableData {
  @JsonIgnore  // Forces Jackson to ignore field
  private final String name;

  @JsonIgnore  // Forces Jackson to ignore field
  private final int count;

  @JsonProperty  // But accessor methods are still annotated
  public String name() { return name; }

  @JsonProperty
  public int count() { return count; }
}

When to enable:

  1. Strict serialization control: Force Jackson to only use accessor methods, not fields
  2. Custom serializers: When implementing custom JsonSerializer/JsonDeserializer
  3. Field naming conflicts: Avoid issues where Jackson might serialize both fields and methods
  4. Security: Prevent accidental serialization of internal field state

Example:

@Value.Style(forceJacksonIgnoreFields = true)
@Value.Immutable
interface User {
  String username();
  String hashedPassword();
}

// Ensures Jackson never accidentally serializes field directly:
// Only the accessor methods are used for JSON

Interaction with Jackson features:

@Value.Style(
  forceJacksonIgnoreFields = true,
  jacksonIntegration = true  // Enable Jackson integration
)
@Value.Immutable
@JsonSerialize  // Custom serializer won't see fields
interface Data {
  String value();
}

Note: This is usually not necessary as Immutables already generates proper @JsonProperty annotations on accessor methods. Enable only if you have specific requirements for field-level control.

See also: jacksonIntegration, forceJacksonPropertyNames


forceJacksonPropertyNames

Default: true

Forces Jackson to use property names that match Immutables-inferred attribute names. This is enabled by default because there are ambiguities with how certain field prefixes work (“get”, “is”). Disable if you want to use Jackson’s naming strategies.

Note: When disabling, make sure you recognize both “get*” and “is*” as attribute name patterns in your style, as Jackson infers default names using JavaBean convention.

Why this exists:

There’s an ambiguity between how Immutables and Jackson infer property names:

@Value.Immutable
interface Config {
  String getName();    // Immutables → "name"
  boolean isEnabled(); // Immutables → "enabled"
}

// Without forceJacksonPropertyNames:
// Jackson might infer different names based on JavaBean conventions:
// - getName() → "name" (Jackson agrees)
// - isEnabled() → Could be "enabled" or "isEnabled" depending on Jackson version

Default behavior (forceJacksonPropertyNames = true):

@Value.Immutable
interface Config {
  String getName();
  boolean isEnabled();
}

// Generated code forces Jackson to use Immutables' names:
class ImmutableConfig {
  @JsonProperty("name")    // Explicit name
  public String getName() { ... }

  @JsonProperty("enabled")  // Explicit name (not "isEnabled")
  public boolean isEnabled() { ... }
}

// JSON output:
// {"name": "...", "enabled": true}

With forceJacksonPropertyNames = false:

@Value.Style(
  forceJacksonPropertyNames = false,
  get = {"get*", "is*"}  // IMPORTANT: Recognize both patterns!
)
@Value.Immutable
interface Config {
  String getName();
  boolean isEnabled();
}

// Generated code lets Jackson use its own naming:
class ImmutableConfig {
  // No @JsonProperty annotations
  public String getName() { ... }
  public boolean isEnabled() { ... }
}

// Jackson applies its naming strategy:
// Could result in {"name": "...", "enabled": true}
// or different names depending on PropertyNamingStrategy

When to disable (set to false):

  1. Using Jackson naming strategies: When you want PropertyNamingStrategy to control names
  2. Custom naming conventions: When you have project-wide Jackson configuration
  3. Migration compatibility: When migrating from JavaBeans and need original naming

Example with naming strategy:

@Value.Style(
  forceJacksonPropertyNames = false,  // Let naming strategy work
  get = {"get*", "is*"}
)
@Value.Immutable
interface UserData {
  String getFirstName();
  String getLastName();
}

// In Jackson configuration:
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

// Results in JSON:
// {"first_name": "...", "last_name": "..."}
// (instead of {"firstName": "...", "lastName": "..."})

Critical warning when disabling:

⚠️ Always use get = {"get*", "is*"} when setting forceJacksonPropertyNames = false

// WRONG - will cause naming conflicts:
@Value.Style(
  forceJacksonPropertyNames = false
  // Missing: get = {"get*", "is*"}
)

// CORRECT:
@Value.Style(
  forceJacksonPropertyNames = false,
  get = {"get*", "is*"}  // Recognize both prefixes
)

Why? Jackson uses JavaBean conventions and recognizes both get and is prefixes. If Immutables only recognizes one prefix, there will be a mismatch.

Interaction with custom @JsonProperty:

@Value.Style(forceJacksonPropertyNames = true)
@Value.Immutable
interface Data {
  @JsonProperty("custom_name")  // Your annotation takes precedence
  String getValue();
}

// Generated code respects your annotation:
@JsonProperty("custom_name")  // Your annotation is preserved
public String getValue() { ... }

Default behavior is recommended:

Unless you have specific requirements for naming strategies, keep the default (true) to avoid naming ambiguities and ensure consistent property names between Immutables and Jackson.

See also: jacksonIntegration, get, init


from

Default: "from"

Naming template for builder method that initializes values from an instance. Set to empty string ("") to suppress generation.

Default behavior (from = “from”):

@Value.Immutable
interface Config {
  String host();
  int port();
  boolean secure();
}

// Generated builder has `from` method:
Config template = ImmutableConfig.builder()
  .host("example.com")
  .port(8080)
  .secure(true)
  .build();

// Initialize new builder from existing instance:
Config modified = ImmutableConfig.builder()
  .from(template)      // Copy all values from template
  .port(8081)          // Override just the port
  .build();
// Result: {host="example.com", port=8081, secure=true}

Custom naming:

@Value.Style(from = "initializeFrom")
@Value.Immutable
interface Config {
  String host();
  int port();
}

// Generated method uses custom name:
Config copy = ImmutableConfig.builder()
  .initializeFrom(template)  // Custom method name
  .port(8081)
  .build();

Disable from generation:

@Value.Style(from = "")  // Empty string disables generation
@Value.Immutable
interface Data {
  String value();
}

// No `from` method generated:
ImmutableData.builder()
  // .from(instance)  // Method doesn't exist!
  .value("test")
  .build();

When to disable:

  1. Strict builders: When using strictBuilder = true, from is automatically disabled (reinitializing values violates strict semantics)
  2. Security: Prevent copying potentially sensitive data from untrusted instances
  3. Forced explicit initialization: Require all values to be set explicitly
  4. Reduced API surface: Minimize builder methods for simpler APIs

Interaction with supertypes:

When your abstract value type has supertypes (interfaces or abstract classes), the from method can accept those supertypes:

interface Base {
  String name();
}

@Value.Immutable
interface Extended extends Base {
  String name();
  int value();
}

// Generated builder accepts both Extended and Base:
Extended e = ...;
Base b = ...;

ImmutableExtended.builder()
  .from(e)  // Accepts Extended - copies name and value
  .build();

ImmutableExtended.builder()
  .from(b)  // Accepts Base - copies only name
  .value(42)  // Must set value separately
  .build();

Dynamic vs static copy behavior:

The behavior of how from handles multiple supertypes is controlled by mergeFromSupertypesDynamically:

interface A { String a(); }
interface B { String b(); }

@Value.Immutable
interface C extends A, B {
  String a();
  String b();
  String c();
}

// With mergeFromSupertypesDynamically = true (default):
Object instance = ...; // Implements both A and B
ImmutableC.builder()
  .from(instance)  // Uses instanceof checks at runtime
  .c("value")
  .build();

// With mergeFromSupertypesDynamically = false:
A instanceA = ...;
B instanceB = ...;
ImmutableC.builder()
  .from(instanceA)  // Compile-time overload for A
  .from(instanceB)  // Compile-time overload for B
  .c("value")
  .build();

Relationship with copyOf:

Both from and copyOf copy values, but serve different purposes:

FeaturefromcopyOf
WhereBuilder methodStatic factory method
PurposeInitialize builder partiallyCreate complete immutable copy
OverridesCan override values afterNo modification after copy
ValidationRuns on build()Runs immediately
PatternBuilder patternFactory method pattern
@Value.Immutable
interface Data {
  String value();
}

Data original = ImmutableData.builder().value("test").build();

// Using copyOf (creates complete immutable instance):
Data copy1 = ImmutableData.copyOf(original);

// Using from (initializes builder, allows modification):
Data copy2 = ImmutableData.builder()
  .from(original)
  .value("modified")  // Can change values
  .build();

Performance considerations:

The from method must copy all attributes:

@Value.Immutable
interface LargeObject {
  String a();
  String b();
  // ... 50 more attributes ...
}

LargeObject obj = ...;

// `from` copies all 52 attributes:
ImmutableLargeObject.builder()
  .from(obj)  // Performs 52 copy operations
  .a("changed")
  .build();

For large objects with many attributes, consider whether you need from or can build incrementally.

Interaction with strictBuilder:

From the Javadoc on strictBuilder:

Also, “from” method (named by from()) will not be generated on builder: it becomes error-inviting to reinitialize builder values.

@Value.Style(
  strictBuilder = true,
  from = "from"  // This will be ignored!
)
@Value.Immutable
interface Data {
  String value();
}

// No `from` method is generated despite `from = "from"`
// because strictBuilder = true disables it

Null handling:

The from method handles nullable attributes correctly:

@Value.Immutable
interface Data {
  @Nullable String optional();
  String required();
}

Data source = ImmutableData.builder()
  .optional(null)
  .required("value")
  .build();

// `from` copies null values:
Data copy = ImmutableData.builder()
  .from(source)  // Copies optional=null, required="value"
  .build();

Usage patterns:

// Pattern 1: Template-based creation
Config prodTemplate = ImmutableConfig.builder()
  .secure(true)
  .timeout(30)
  .retries(3)
  .build();

Config prodServer1 = ImmutableConfig.builder()
  .from(prodTemplate)
  .host("server1.example.com")
  .build();

Config prodServer2 = ImmutableConfig.builder()
  .from(prodTemplate)
  .host("server2.example.com")
  .build();

// Pattern 2: Incremental modification
Config config = getBaseConfig();
for (Override override : overrides) {
  config = ImmutableConfig.builder()
    .from(config)
    .apply(override)  // Custom method applying override
    .build();
}

// Pattern 3: Merging from multiple sources
Result result = ImmutableResult.builder()
  .from(partialResult1)  // Get some attributes
  .from(partialResult2)  // Overwrite/add more attributes
  .finalValue("computed")
  .build();

See also: copyOf, mergeFromSupertypesDynamically, strictBuilder


generateSuppressAllWarnings

Default: true

When true, generates @SuppressWarnings("all") annotation on all generated classes and methods. When false, no warning suppression is added, allowing compiler warnings to be visible.

Note: To suppress warnings issued by Immutables itself (not in generated code), use explicit annotations @SuppressWarning("immutables") or @SuppressWarning("all") on your abstract value types.

Default behavior (generateSuppressAllWarnings = true):

@Value.Immutable
interface Data {
  String value();
}

// Generated class:
@SuppressWarnings("all")  // Suppresses all compiler warnings
final class ImmutableData implements Data {
  @SuppressWarnings("all")
  public static Builder builder() { ... }

  @SuppressWarnings("all")
  private final String value;
  // ...
}

With generateSuppressAllWarnings = false:

@Value.Style(generateSuppressAllWarnings = false)
@Value.Immutable
interface Data {
  String value();
}

// Generated class has NO @SuppressWarnings:
final class ImmutableData implements Data {
  // No @SuppressWarnings("all") annotation

  public static Builder builder() { ... }

  private final String value;
  // Compiler warnings (if any) will be visible
}

Why warnings are suppressed by default:

Generated code often triggers compiler warnings that are not actionable by developers:

  1. Unchecked warnings: Generic type operations that are known to be safe
  2. Unused parameters: Internal methods with standard signatures
  3. Deprecation warnings: When using deprecated types from your abstract value type
  4. Raw types: Internal implementation details using raw types safely
  5. Dead code: Conditional compilation paths
  6. Missing documentation: Generated methods don’t need JavaDoc

When to disable (set to false):

  1. Strict compiler configuration: When you require all code (including generated) to be warning-free
  2. Code quality tools: When running static analysis tools that should examine generated code
  3. Debugging: When investigating issues in generated code
  4. Learning: When studying what code is generated

Example of exposing warnings (value-fixture/src/org/immutables/fixture/style/LightOnAnnotations.java:15):

@Value.Style(
  generateSuppressAllWarnings = false,  // Expose all warnings
  allowedClasspathAnnotations = {Override.class}
)

Suppressing specific Immutables warnings:

Even with generateSuppressAllWarnings = false, you can suppress Immutables-specific warnings on your abstract value types:

@Value.Style(generateSuppressAllWarnings = false)
@SuppressWarnings("immutables")  // Suppress only Immutables warnings
@Value.Immutable
interface Data {
  String value();
}

Or suppress all warnings on abstract type:

@Value.Style(generateSuppressAllWarnings = false)
@SuppressWarnings("all")  // Suppress warnings from generated code
@Value.Immutable
interface Data {
  String value();
}

Common warnings in generated code:

When generateSuppressAllWarnings = false, you might see:

@Value.Style(generateSuppressAllWarnings = false)
@Value.Immutable
interface Data {
  @Deprecated
  String legacyField();
}

// Generated code:
class ImmutableData {
  // Warning: [deprecation] legacyField() in Data has been deprecated
  @Deprecated
  public String legacyField() { ... }

  // Warning: [unchecked] unchecked conversion
  List list = new ArrayList();  // In certain internal operations
}

Interaction with build tools:

<!-- Maven: Fail build on warnings -->
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <failOnWarning>true</failOnWarning>
      </configuration>
    </plugin>
  </plugins>
</build>

If you enable failOnWarning in Maven, you typically want generateSuppressAllWarnings = true (the default) to avoid build failures from generated code warnings.

Gradle configuration:

tasks.withType(JavaCompile) {
  options.compilerArgs << "-Werror"  // Treat warnings as errors
}

// With generateSuppressAllWarnings = false, this might fail
// if generated code produces warnings

Effect on different warning categories:

Warning CategorySuppressed with trueVisible with false
Unchecked
Deprecation
Raw types
Unused
Fallthrough
Serial
All others

Best practices:

  1. Keep default for production: Use generateSuppressAllWarnings = true (default) in production code
  2. Disable for debugging: Temporarily set to false when investigating generated code issues
  3. Use immutables-specific suppression: Use @SuppressWarnings("immutables") on abstract types when needed
  4. Don’t mix with strict compiler flags: If using -Werror or failOnWarning, keep the default true

Alternative: Suppress per-type:

Instead of disabling globally, suppress warnings on specific abstract value types:

// Keep generateSuppressAllWarnings = true (default)

@SuppressWarnings("all")  // Suppress only for this type
@Value.Immutable
interface ProblematicData {
  @Deprecated
  String legacyField();
}

@Value.Immutable  // No suppression needed
interface CleanData {
  String modernField();
}

See also: allowedClasspathAnnotations


get

Default: "get*" (single-element array)

Array of naming patterns to recognize and strip from accessor method names to derive attribute names. If no patterns match, the raw accessor name is used as the attribute name.

Default behavior (get = get*):

@Value.Immutable
interface Config {
  String getName();    // → attribute "name"
  int getPort();       // → attribute "port"
  boolean enabled();   // → attribute "enabled" (no prefix)
}

JavaBean style (most common customization):

@Value.Style(get = {"is*", "get*"})  // Order matters for matching
@Value.Immutable
interface Config {
  String getName();      // → attribute "name" (matches "get*")
  boolean isEnabled();   // → attribute "enabled" (matches "is*")
  int port();            // → attribute "port" (no match, used as-is)
}

// Generated builder:
ImmutableConfig.builder()
  .name("localhost")     // Not getName()
  .enabled(true)         // Not isEnabled()
  .port(8080)            // port (unchanged)
  .build();

Kotlin-style properties:

@Value.Style(get = "*")  // No prefix at all
@Value.Immutable
interface Data {
  String name();    // → attribute "name"
  int age();        // → attribute "age"
}

Suffix patterns:

You can also use suffix patterns to detect and strip suffixes:

@Value.Style(get = {"get*", "is*", "*Property"})
@Value.Immutable
interface Val {
  int getProp();           // → "prop" (matches "get*")
  boolean isEmpty();        // → "empty" (matches "is*")
  String fooProperty();     // → "foo" (matches "*Property")
  double value();           // → "value" (no match)
}

Pattern matching order:

Patterns are matched in the order specified. First match wins:

@Value.Style(get = {"is*", "get*"})
@Value.Immutable
interface Data {
  String getIsActive();  // → "Active" (matches "is*" first!)
}

@Value.Style(get = {"get*", "is*"})
@Value.Immutable
interface Data {
  String getIsActive();  // → "IsActive" (matches "get*" first!)
}

Mixing prefixes and suffixes:

@Value.Style(get = {"get*", "*Value", "*Attr"})
@Value.Immutable
interface Data {
  String getName();         // → "name" (prefix pattern)
  int heightValue();        // → "height" (suffix pattern)
  boolean enabledAttr();    // → "enabled" (suffix pattern)
  double width();           // → "width" (no pattern)
}

IMPORTANT: This is a detection pattern, not a formatting pattern:

The get attribute controls how Immutables reads your accessor names to determine attribute names. It does NOT control how generated methods are named. For that, use init, with, build, etc.

@Value.Style(get = {"is*", "get*"})  // DETECTION
@Value.Immutable
interface Config {
  boolean isEnabled();  // Detected as attribute "enabled"
}

// Generated code still uses original accessor name in implementation:
class ImmutableConfig {
  public boolean isEnabled() { ... }  // Keeps original name
}

// But builder uses detected attribute name:
ImmutableConfig.builder()
  .enabled(true)  // Uses "enabled", not "isEnabled"
  .build();

Interaction with init:

@Value.Style(
  get = {"is*", "get*"},  // Detection: strip "is"/"get"
  init = "set*"            // Formatting: add "set" prefix
)
@Value.Immutable
interface Config {
  String getName();      // Detected as "name"
  boolean isEnabled();   // Detected as "enabled"
}

// Generated builder:
ImmutableConfig.builder()
  .setName("test")     // init pattern adds "set"
  .setEnabled(true)    // init pattern adds "set"
  .build();

Example from fixtures (value-fixture/src/org/immutables/fixture/style/BeanAccessors.java:23):

@Value.Style(get = {"is*", "get*"}, init = "set*")

Common patterns:

// JavaBean style (most common):
@Value.Style(get = {"is*", "get*"})

// Kotlin/Scala style (no prefix):
@Value.Style(get = "*")

// Custom prefix:
@Value.Style(get = "fetch*")

// Multiple custom patterns:
@Value.Style(get = {"get*", "is*", "has*", "can*"})

Pattern syntax:

  • "*" - Matches entire name (no stripping)
  • "prefix*" - Strips prefix
  • "*suffix" - Strips suffix
  • No wildcards in middle - "pre*fix" is NOT supported

Edge cases:

@Value.Style(get = {"get*"})
@Value.Immutable
interface Data {
  String get();     // → attribute "get" (no match, "get" alone has nothing after "get")
  String getName(); // → attribute "Name" (strips "get", keeps capital N)
}

// Recommendation: accessor names should start with lowercase after prefix
interface Data {
  String getName(); // → "name" (better!)
}

Interaction with Jackson:

When using Jackson with custom get patterns, ensure forceJacksonPropertyNames is configured correctly:

@Value.Style(
  get = {"is*", "get*"},
  forceJacksonPropertyNames = true  // Immutables forces "enabled" in JSON
)
@Value.Immutable
interface Config {
  boolean isEnabled();  // JSON property: "enabled"
}

Best practices:

  1. Use JavaBean patterns for compatibility: get = {"is*", "get*"} works well with Jackson, JPA, and other frameworks
  2. Be consistent: Don’t mix styles (e.g., some with “get”, some without) in the same project
  3. Order matters: Put more specific patterns first: get = {"is*", "get*"} not get = {"get*", "is*"}
  4. Apply at package level: Use meta-annotation or package-info.java to apply consistently

Meta-annotation example:

@Value.Style(get = {"is*", "get*"}, init = "set*")
@Target({ElementType.PACKAGE, ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface BeanStyle {}

// Then use throughout project:
@BeanStyle
@Value.Immutable
interface Config { ... }

See also: init, with, forceJacksonPropertyNames


getBuilder

Default: "*Builder"

Naming template for methods that retrieve nested builder instances when attributeBuilderDetection is enabled. The * is replaced with the attribute name.

*Default behavior (getBuilder = “Builder”):

@Value.Style(attributeBuilderDetection = true)
@Value.Immutable
interface Container {
  @Value.Immutable
  interface Item {
    String name();
    int value();
  }

  Item item();  // Nested immutable
}

// Generated builder:
ImmutableContainer.builder()
  .itemBuilder()       // Gets builder for "item" attribute
    .name("test")      // Build nested Item
    .value(42)
    .build();

Custom naming:

@Value.Style(
  attributeBuilderDetection = true,
  getBuilder = "builderFor*"
)
@Value.Immutable
interface Container {
  Item item();
}

// Generated method uses custom name:
ImmutableContainer.builder()
  .builderForItem()    // Custom name "builderFor" + "Item"
    .name("test")
    .value(42)
    .build();

Another naming example:

@Value.Style(
  attributeBuilderDetection = true,
  getBuilder = "*NestedBuilder"
)
@Value.Immutable
interface Container {
  Item item();
}

// Generated:
ImmutableContainer.builder()
  .itemNestedBuilder()  // "item" + "NestedBuilder"
    .name("test")
    .value(42)
    .build();

When is this used?

This only applies when ALL of these conditions are met:

  1. attributeBuilderDetection is true
  2. An attribute’s type is itself a @Value.Immutable type
  3. The nested immutable type has a builder (not just a constructor)

Example with multiple nested builders:

@Value.Style(
  attributeBuilderDetection = true,
  getBuilder = "edit*"
)
@Value.Immutable
interface Document {
  @Value.Immutable
  interface Header {
    String title();
    String author();
  }

  @Value.Immutable
  interface Body {
    String content();
  }

  Header header();
  Body body();
}

// Usage:
ImmutableDocument doc = ImmutableDocument.builder()
  .editHeader()          // getBuilder template "edit*"
    .title("My Doc")
    .author("Alice")
  .editBody()            // getBuilder template "edit*"
    .content("Hello")
  .build();

Difference from setBuilder:

  • getBuilder: Retrieves/accesses an existing builder for modification
  • setBuilder: Replaces the builder instance entirely
@Value.Style(
  attributeBuilderDetection = true,
  getBuilder = "*Builder",
  setBuilder = "set*Builder"
)
@Value.Immutable
interface Container {
  Item item();
}

// getBuilder - retrieve for modification:
builder.itemBuilder()           // Get builder
  .name("test")
  .value(42);

// setBuilder - replace entirely:
builder.setItemBuilder(
  ImmutableItem.builder().name("test").value(42)
);

Interaction with collection attributes:

For collection attributes with nested immutables, use getBuilders (plural) instead:

@Value.Style(
  attributeBuilderDetection = true,
  getBuilder = "*Builder",      // For single attributes
  getBuilders = "*Builders"     // For collection attributes
)
@Value.Immutable
interface Container {
  Item singleItem();        // Uses getBuilder
  List<Item> items();       // Uses getBuilders
}

Fluent API example:

@Value.Style(
  attributeBuilderDetection = true,
  getBuilder = "configure*"
)
@Value.Immutable
interface ServerConfig {
  DatabaseConfig database();
  CacheConfig cache();
}

// Fluent configuration:
ImmutableServerConfig config = ImmutableServerConfig.builder()
  .configureDatabase()
    .host("localhost")
    .port(5432)
  .configureCache()
    .maxSize(1000)
    .ttlSeconds(300)
  .build();

Note: The nested builder is retrieved each time you call the getter method. If you need to keep a reference to the builder for later modification, store it:

ImmutableContainer.Builder containerBuilder = ImmutableContainer.builder();

// Get reference to nested builder:
ImmutableItem.Builder itemBuilder = containerBuilder.itemBuilder();

// Modify nested builder multiple times:
itemBuilder.name("first");
// ... later ...
itemBuilder.value(42);

// Build container (includes nested item):
Container container = containerBuilder.build();

See also: attributeBuilderDetection, setBuilder, getBuilders, addBuilder


getBuilders

Default: "*Builders"

Naming template for methods that retrieve an immutable list of nested builders for collection attributes when attributeBuilderDetection is enabled. The * is replaced with the attribute name.

*Default behavior (getBuilders = “Builders”):

@Value.Style(attributeBuilderDetection = true)
@Value.Immutable
interface Container {
  @Value.Immutable
  interface Item {
    String name();
    int value();
  }

  List<Item> items();  // Collection of nested immutables
}

// Generated builder:
ImmutableContainer.Builder builder = ImmutableContainer.builder();

// Add items via nested builders:
builder.addItemBuilder()
  .name("item1")
  .value(10);

builder.addItemBuilder()
  .name("item2")
  .value(20);

// Retrieve all builders:
List<ImmutableItem.Builder> builders = builder.itemsBuilders();
// Returns immutable list of the two builders added above

Custom naming:

@Value.Style(
  attributeBuilderDetection = true,
  getBuilders = "*BuilderList"
)
@Value.Immutable
interface Container {
  List<Item> items();
}

// Generated method uses custom name:
List<ImmutableItem.Builder> builders = builder.itemsBuilderList();

When is this used?

This only applies when ALL of these conditions are met:

  1. attributeBuilderDetection is true
  2. An attribute is a collection type (List, Set, etc.)
  3. The collection’s element type is itself a @Value.Immutable type
  4. The nested immutable type has a builder (not just a constructor)

Difference from getBuilder:

  • getBuilder: For single nested immutable attributes → returns one builder
  • getBuilders: For collection of nested immutables → returns immutable list of builders
@Value.Style(
  attributeBuilderDetection = true,
  getBuilder = "*Builder",      // Singular
  getBuilders = "*Builders"     // Plural
)
@Value.Immutable
interface Container {
  Item singleItem();      // Uses getBuilder() → one builder
  List<Item> items();     // Uses getBuilders() → list of builders
}

Container.Builder builder = ...;
ImmutableItem.Builder single = builder.singleItemBuilder();   // One builder
List<ImmutableItem.Builder> list = builder.itemsBuilders();   // List of builders

Example with modification:

@Value.Style(attributeBuilderDetection = true)
@Value.Immutable
interface TodoList {
  @Value.Immutable
  interface Task {
    String title();
    boolean done();
  }

  List<Task> tasks();
}

// Build todo list:
ImmutableTodoList.Builder todoBuilder = ImmutableTodoList.builder();

// Add tasks via builders:
todoBuilder.addTaskBuilder().title("Task 1").done(false);
todoBuilder.addTaskBuilder().title("Task 2").done(false);
todoBuilder.addTaskBuilder().title("Task 3").done(true);

// Get all task builders:
List<ImmutableTask.Builder> taskBuilders = todoBuilder.tasksBuilders();

// Modify existing builders (e.g., mark all as done):
for (ImmutableTask.Builder taskBuilder : taskBuilders) {
  taskBuilder.done(true);
}

// Build final todo list with all tasks marked done:
TodoList allDone = todoBuilder.build();

Immutability of returned list:

The returned list is immutable - you cannot add or remove builders from it:

List<ImmutableItem.Builder> builders = builder.itemsBuilders();
builders.add(newBuilder);  // UnsupportedOperationException!

To add builders, use addBuilder on the parent builder.

Use cases:

  1. Batch modifications: Modify all nested builders at once
  2. Conditional logic: Iterate through builders and apply conditional changes
  3. Inspection: Check the current state of nested builders before building
  4. Builder transformation: Transform or filter builders based on criteria
// Use case: Apply conditional logic to nested builders
ImmutableDocument.Builder docBuilder = ImmutableDocument.builder();
docBuilder.addSectionBuilder().title("Intro").content("...");
docBuilder.addSectionBuilder().title("Body").content("...");
docBuilder.addSectionBuilder().title("Conclusion").content("...");

// Get all section builders and add page numbers:
List<ImmutableSection.Builder> sections = docBuilder.sectionsBuilders();
for (int i = 0; i < sections.size(); i++) {
  sections.get(i).pageNumber(i + 1);
}

Document doc = docBuilder.build();

See also: attributeBuilderDetection, getBuilder, addBuilder, addAllBuilder


headerComments

Default: false

Enable if you needed to copy header comments from an originating source file with abstract types to generated (derived) implementation classes. Header comments are comments preceding package declaration statement. It could be used to copy license headers or even special pragma comments (such as //-no-import-rewrite). It is off by default because not often needed (as generated files are transient and not stored in version control), but adds up to the processing time. Requires annotation processor to access sources

When enabled, copies header comments (comments before the package declaration) from source files to generated classes. Useful for license headers or pragma comments.

What are header comments?

Header comments are any comments that appear before the package statement in a Java file:

/*
 * Copyright (C) 2025 Example Corp
 * Licensed under Apache License 2.0
 */

// Additional header comment

package com.example;  // Package declaration

// This comment is NOT a header comment (appears after package)

@Value.Immutable
interface Data {
  String value();
}

Default behavior (headerComments = false):

// Source file: Data.java
/*
 * Copyright (C) 2025 Example Corp
 */
package com.example;

@Value.Immutable
interface Data {
  String value();
}

// Generated file: ImmutableData.java
package com.example;  // No copyright header!

@Generated
final class ImmutableData implements Data {
  ...
}

With headerComments = true:

@Value.Style(headerComments = true)
// Source file: Data.java
/*
 * Copyright (C) 2025 Example Corp
 * Licensed under Apache License 2.0
 */
package com.example;

@Value.Immutable
interface Data {
  String value();
}

// Generated file: ImmutableData.java
/*
 * Copyright (C) 2025 Example Corp
 * Licensed under Apache License 2.0
 */
package com.example;  // Header comment copied!

@Generated
final class ImmutableData implements Data {
  ...
}

Use cases:

  1. License headers: Copy license information to generated code
  2. Copyright notices: Maintain copyright headers in generated files
  3. Pragma comments: Copy special compiler or IDE directives
  4. Compliance: Meet legal requirements for generated code attribution

Example with pragma comments:

Some build tools or IDEs use special pragma comments:

//-no-import-rewrite

package com.example;

@Value.Style(headerComments = true)
@Value.Immutable
interface Data {
  String value();
}

// Generated file will include: //-no-import-rewrite
// Disable auto-import rewriting for this file

Why disabled by default:

  1. Generated files are transient: Most projects don’t commit generated files to version control
  2. Processing overhead: Parsing and copying comments adds to annotation processing time
  3. Build-time generation: Files regenerated during build don’t need persistent headers
  4. IDE noise: License headers in generated files can clutter IDE views
  5. Source Access: Requires annotation processor to access sources

When to enable:

  • Legal requirements: When generated code must include copyright/license headers
  • Source control: If you DO commit generated files to version control
  • Code distribution: When distributing generated code separately
  • Compliance audits: When audits require headers in all source files

Performance impact:

@Value.Style(headerComments = true)  // Adds ~5-10ms per file to processing

// For projects with 100 immutable classes:
// Additional processing time: ~0.5-1 second per compilation

The impact is minimal but can add up in large projects.

Alternative approach:

Instead of enabling headerComments, you can:

  1. Use a post-processing tool to add headers to generated files
  2. Configure your build to add headers during packaging
  3. Use IDE templates to add headers when viewing generated files

See also: allowedClasspathAnnotations


immutableCopyOfRoutines

Default: {} (empty array)

You can provide classes which must contain copyOf method with relevant overloads which should not have ambiguous cases as it will be fully a subject to JLS rules of static imports and compile time overload resolution.

The major use case is custom validation and normalization of the attribute values by types. Validations specific to a value object or its attributes could be performed using…

Note: This attribute-style is experimental and may be changed in near releases. The manner in which routines are applied…

Classes containing static immutableCopyOf methods that will be statically imported and used for custom validation and normalization of attribute values by type.

How it works:

When you specify routine classes, the generated code will:

  1. Add static imports for the immutableCopyOf methods
  2. Call these methods when setting attribute values
  3. Use the returned (validated/normalized) value instead of the raw input

Basic example:

// Define validation/normalization routines:
public class StringRoutines {
  public static String immutableCopyOf(String s) {
    if (s == null) return "";
    return s.trim().intern();  // Normalize: trim and intern
  }
}

public class IntRoutines {
  public static int immutableCopyOf(int i) {
    return Math.max(0, i);  // Validate: ensure non-negative
  }
}

// Apply to style:
@Value.Style(
  immutableCopyOfRoutines = {StringRoutines.class, IntRoutines.class}
)
@Value.Immutable
interface Data {
  String name();
  int count();
}

// Generated code includes:
// import static com.example.StringRoutines.immutableCopyOf;
// import static com.example.IntRoutines.immutableCopyOf;
//
// ImmutableData(String name, int count) {
//   this.name = immutableCopyOf(name);    // Normalizes string
//   this.count = immutableCopyOf(count);  // Validates int
// }

// Usage:
Data data = ImmutableData.builder()
  .name("  Alice  ")  // Input has spaces
  .count(-5)          // Negative input
  .build();

data.name();  // Returns "Alice" (trimmed and interned)
data.count(); // Returns 0 (normalized to non-negative)

Type-based dispatch:

The processor uses Java’s overload resolution to match the correct immutableCopyOf method based on parameter type:

public class Routines {
  public static String immutableCopyOf(String s) {
    return s == null ? "" : s.toUpperCase();
  }

  public static List<String> immutableCopyOf(List<String> list) {
    // Normalize list elements
    return list.stream()
      .map(String::toUpperCase)
      .collect(Collectors.toList());
  }

  public static Integer immutableCopyOf(Integer i) {
    return i == null ? 0 : i;
  }
}

@Value.Style(immutableCopyOfRoutines = {Routines.class})
@Value.Immutable
interface Data {
  String name();          // Uses immutableCopyOf(String)
  List<String> tags();    // Uses immutableCopyOf(List<String>)
  Integer count();        // Uses immutableCopyOf(Integer)
}

Validation example:

public class Validators {
  public static String immutableCopyOf(String email) {
    if (email == null || !email.contains("@")) {
      throw new IllegalArgumentException("Invalid email: " + email);
    }
    return email.toLowerCase().trim();
  }

  public static int immutableCopyOf(int port) {
    if (port < 1 || port > 65535) {
      throw new IllegalArgumentException("Invalid port: " + port);
    }
    return port;
  }
}

@Value.Style(immutableCopyOfRoutines = {Validators.class})
@Value.Immutable
interface ServerConfig {
  String adminEmail();  // Validated as email
  int port();           // Validated as valid port
}

// This will throw IllegalArgumentException:
ImmutableServerConfig.builder()
  .adminEmail("not-an-email")  // No @
  .port(99999)                 // Out of range
  .build();

Normalization example:

public class PathNormalizer {
  public static String immutableCopyOf(String path) {
    if (path == null) return "/";
    // Normalize path separators and remove trailing slash
    path = path.replace('\\', '/');
    if (path.endsWith("/") && path.length() > 1) {
      path = path.substring(0, path.length() - 1);
    }
    return path;
  }
}

@Value.Style(immutableCopyOfRoutines = {PathNormalizer.class})
@Value.Immutable
interface FileConfig {
  String basePath();
  String logPath();
}

FileConfig config = ImmutableFileConfig.builder()
  .basePath("C:\\Users\\Alice\\")     // Windows path with trailing slash
  .logPath("/var/logs/")              // Unix path with trailing slash
  .build();

config.basePath();  // Returns "C:/Users/Alice"
config.logPath();   // Returns "/var/logs"

Important constraints:

  1. Method signature: Must be public static T immutableCopyOf(T value)
  2. No ambiguity: Overloads must not be ambiguous per JLS rules
  3. Type matching: Exact type match required (no covariance)
  4. Exception handling: Exceptions thrown during validation will propagate
  5. Performance: Called for every attribute value, so should be efficient

Experimental status:

This feature is marked as experimental and may change in future releases:

  • The manner in which routines are applied may be refined
  • API for defining routines may evolve
  • Integration with other features may change

Use with caution and be prepared for potential breaking changes in future Immutables versions.

When to use:

  • Type-level validation: Validate all strings/integers/etc. consistently
  • Normalization: Ensure consistent format (trim, lowercase, etc.)
  • Interning: Reduce memory by interning strings
  • Cross-cutting concerns: Apply same logic across multiple value types

When NOT to use:

  • Attribute-specific validation: Use @Value.Check instead
  • Complex validation: Use builder validation or factory methods
  • Side effects: Don’t use for logging, metrics, etc.
  • Stateful operations: Routines should be pure functions

Alternative: @Value.Check:

For attribute-specific validation, use @Value.Check:

@Value.Immutable
interface User {
  String email();

  @Value.Check
  default void check() {
    if (!email().contains("@")) {
      throw new IllegalStateException("Invalid email");
    }
  }
}

See also: @Value.Check, from, of


implementationNestedInBuilder

Default: false

By default, builder is generated as inner builder class nested in immutable value class. Setting this to true will flip the picture — immutable implementation class will be nested inside builder, which will be top level class. In case if visibility() is set to ImplementationVisibility.PRIVATE this feature is turned on automatically.

When true, reverses the typical nesting structure: the builder becomes the top-level class and the immutable implementation becomes a nested inner class inside the builder.

Default structure (implementationNestedInBuilder = false):

@Value.Immutable
interface Value {
  String data();
}

// Generated structure:
public final class ImmutableValue implements Value {  // Top-level immutable
  private final String data;

  public static class Builder {  // Nested builder
    public Builder data(String data) { ... }
    public ImmutableValue build() { ... }
  }

  public static Builder builder() {
    return new Builder();
  }
}

// Usage:
ImmutableValue value = ImmutableValue.builder()  // Access via immutable class
  .data("test")
  .build();

Flipped structure (implementationNestedInBuilder = true):

@Value.Style(implementationNestedInBuilder = true)
@Value.Immutable
interface Value {
  String data();
}

// Generated structure:
public final class ImmutableValueBuilder {  // Top-level builder
  public ImmutableValueBuilder data(String data) { ... }
  public Value build() { ... }  // Returns abstract type

  static final class Value implements Value {  // Nested immutable (package-private or private)
    private final String data;
    // ...
  }
}

// Usage:
Value value = new ImmutableValueBuilder()  // Access via builder class
  .data("test")
  .build();

Automatic activation:

This feature is automatically enabled when visibility is set to PRIVATE:

@Value.Style(visibility = ImplementationVisibility.PRIVATE)
@Value.Immutable
interface Value {
  String data();
}

// implementationNestedInBuilder automatically becomes true
// because private immutable implementation must be hidden

Why use this?

  1. Hide implementation: Keep immutable implementation private or package-private
  2. API surface control: Expose only builder and abstract type in public API
  3. Encapsulation: Prevent direct access to implementation class
  4. Forced builder usage: Users must use builder (can’t access implementation directly)

Example with private implementation (value-fixture/src/org/immutables/fixture/style/NestingClassOrBuilder.java:24):

@Value.Style(
  implementationNestedInBuilder = true,
  visibility = ImplementationVisibility.PRIVATE,
  builderVisibility = BuilderVisibility.PACKAGE
)
@Value.Immutable
interface Config {
  String host();
  int port();
}

// Generated:
final class ImmutableConfigBuilder {  // Package-private builder
  public ImmutableConfigBuilder host(String host) { ... }
  public ImmutableConfigBuilder port(int port) { ... }
  public Config build() { ... }  // Returns abstract type

  private static final class Value implements Config {  // Private implementation
    // Hidden from users
  }
}

// Usage:
Config config = new ImmutableConfigBuilder()
  .host("localhost")
  .port(8080)
  .build();

// Cannot access:
// ImmutableConfigBuilder.Value  // Compile error: private class

Interaction with visibility settings:

visibilitybuilderVisibilityimplementationNestedInBuilderResult
SAMEPUBLICfalse (default)Standard: public immutable, nested public builder
PRIVATEPUBLICtrue (automatic)Public builder top-level, private immutable nested
PACKAGEPUBLICfalse (default)Package immutable top-level, public nested builder
PUBLICPRIVATEfalsePublic immutable, private nested builder (builder hidden)

Use cases:

  1. Library API design: Hide implementation, expose only interface and builder
  2. Internal-only types: Package-private builders for internal use
  3. Security: Prevent reflection access to implementation class
  4. Reduced API surface: Users only see abstract type and builder

Example: Public builder, private implementation:

@Value.Style(
  implementationNestedInBuilder = true,
  visibility = ImplementationVisibility.PRIVATE,
  builderVisibility = BuilderVisibility.PUBLIC
)
@Value.Immutable
public interface User {
  String username();
  String email();
}

// Generated:
public final class ImmutableUserBuilder {  // Public builder
  public ImmutableUserBuilder username(String username) { ... }
  public ImmutableUserBuilder email(String email) { ... }
  public User build() { ... }

  private static final class Value implements User {  // Private - hidden
    // ...
  }
}

// Users can only:
User user = new ImmutableUserBuilder()
  .username("alice")
  .email("alice@example.com")
  .build();

// Cannot do:
// ImmutableUser.builder()  // Class doesn't exist as top-level
// new ImmutableUser(...)   // Implementation is private

Builder construction patterns:

With implementationNestedInBuilder = true, builder construction changes:

// Default false - access via immutable class:
ImmutableValue.builder().data("test").build();

// Set to true - access via builder class directly:
new ImmutableValueBuilder().data("test").build();

// Or use builder() method template:
@Value.Style(
  implementationNestedInBuilder = true,
  builder = "new"  // Use 'new' keyword for construction
)

Interaction with overshadowImplementation:

@Value.Style(
  implementationNestedInBuilder = true,
  visibility = ImplementationVisibility.PRIVATE,
  overshadowImplementation = true  // Methods return abstract type
)
@Value.Immutable
interface Data {
  String value();
}

// All builder methods return abstract type 'Data', not implementation
Data data = new ImmutableDataBuilder()
  .value("test")
  .build();  // Returns Data (abstract type)

Trade-offs:

Pros:

  • Stronger encapsulation
  • Cleaner public API
  • Forces builder usage
  • Hides implementation details

Cons:

  • Different construction pattern (non-standard)
  • Can’t access immutable class directly
  • May be confusing for users expecting standard pattern
  • Breaks some reflection-based frameworks expecting top-level immutable class

See also: visibility, builderVisibility, overshadowImplementation


includeHashCode

Default: "" (empty, not used)

Examples:

includeHashCode = "this.baseHashCode()"
includeHashCode = "super.hashCode()"
includeHashCode = "getClass().hashCode()"
includeHashCode = "[[type]].class.hashCode()"
includeHashCode = "(\"[[type]]\".length() + 1)"

Available since 2.3. Verbatim code snippet to include as part of the generated hashCode() method. The placeholder [[type]] is replaced with the simple name of the abstract value type.

Note: this style option will be ignored if hashCode will be manually written in the abstract value class or underrideHashCode() will be used for the same purpose

Default behavior (includeHashCode = ""):

@Value.Immutable
interface Person {
  String name();
  int age();
}

// Generated hashCode:
@Override
public int hashCode() {
  int h = 5381;
  h += (h << 5) + name.hashCode();
  h += (h << 5) + age;
  return h;
}

With includeHashCode:

@Value.Style(includeHashCode = "getClass().hashCode()")
@Value.Immutable
interface Person {
  String name();
  int age();
}

// Generated hashCode includes class hash:
@Override
public int hashCode() {
  int h = 5381;
  h += (h << 5) + getClass().hashCode();
  h += (h << 5) + name.hashCode();
  h += (h << 5) + age;
  return h;
}

Why include additional hashCode components?

  1. Inheritance hierarchies: Distinguish subclasses with same attribute values
  2. Type discrimination: Ensure different types hash differently
  3. Custom logic: Include base class or supertype hash
  4. Hash stability: Add constant factors for hash distribution

Example: Class-based hash discrimination:

@Value.Style(includeHashCode = "getClass().hashCode()")
@Value.Immutable
interface Animal {
  String name();
}

@Value.Immutable
interface Dog extends Animal {
  String name();
}

@Value.Immutable
interface Cat extends Animal {
  String name();
}

// Without includeHashCode:
Dog dog = ImmutableDog.builder().name("Buddy").build();
Cat cat = ImmutableCat.builder().name("Buddy").build();

// regular hashCode
dog.hashCode() == cat.hashCode()  // May be true (same name!)

// With includeHashCode = "getClass().hashCode()":
dog.hashCode() != cat.hashCode()  // Different classes, different hashes

Example: [[type]] interpolation:

@Value.Style(includeHashCode = "[[type]].class.hashCode()")
@Value.Immutable
interface Person {
  String name();
}

// Generated hashCode uses type name:
@Override
public int hashCode() {
  int h = 5381;
  h += (h << 5) + Person.class.hashCode();  // [[type]] replaced with "Person"
  h += (h << 5) + name.hashCode();
  return h;
}

Example: Super class hash:

@Value.Style(includeHashCode = "super.hashCode()")
@Value.Immutable
abstract class BaseEntity {
  abstract long id();

  @Override
  public int hashCode() {
    return 941;
  }
}

@Value.Immutable
abstract class User extends BaseEntity {
  abstract String username();
}

// Generated hashCode for User:
@Override
public int hashCode() {
  int h = 5381;
  h += (h << 5) + super.hashCode(); // Includes BaseEntity hashCode
  h += (h << 5) + Long.hashCode(id());
  h += (h << 5) + username.hashCode();
  return h;
}

Example: Custom base method:

@Value.Style(includeHashCode = "this.baseHashCode()")
@Value.Immutable
interface Config {
  String host();
  int port();

  default int baseHashCode() {
    return "Config".hashCode();  // Custom hash base
  }
}

// Generated hashCode for Config:
@Override
public int hashCode() {
  int h = 5381;
  h += (h << 5) + this.baseHashCode();
  h += (h << 5) + host.hashCode();
  h += (h << 5) + port;
  return h;
}

Example: String length constant:

@Value.Style(includeHashCode = "(\"[[type]]\".length() + 1)")
@Value.Immutable
interface Person {
  String name();
}

// Generated:
@Override
public int hashCode() {
  int h = 5381;
  h += (h << 5) + ("Person".length() + 1);  // len + 1 = 7
  h += (h << 5) + host.hashCode();
  h += (h << 5) + port;
  return h;
}

When this is ignored:

// Case 1: Manual hashCode override
@Value.Immutable
interface Data {
  String value();

  default int hashingCodes() {
    return value().hashCode();  // Manual implementation
  }
}

// Case 2: underrideHashCode used
@Value.Style(
  includeHashCode = "getClass().hashCode()",  // IGNORED
  underrideHashCode = "hashingCodes"  // This takes precedence
)
@Value.Immutable
interface Data {
  String value();

  default int hashCodeImpl() {
    return 42;  // Custom hash via underride
  }
}

Common patterns:

// Pattern 1: Class-based discrimination (most common)
includeHashCode = "getClass().hashCode()"

// Pattern 2: Type literal discrimination
includeHashCode = "[[type]].class.hashCode()"

// Pattern 3: Superclass integration
includeHashCode = "super.hashCode()"

// Pattern 4: Custom base method
includeHashCode = "this.baseHashCode()"

// Pattern 5: Constant factor
includeHashCode = "31"  // Simple prime number

// Pattern 6: Type name length (unusual but valid)
includeHashCode = "\"[[type]]\".length()"

Important notes:

  • Code is verbatim: The string is inserted directly into generated code
  • Compile-time “safety”: Invalid code will cause compilation errors in generated class
  • Performance: Keep the computation lightweight (called frequently)
  • null safety: Ensure included code doesn’t throw NullPointerException

Use cases:

  1. Polymorphic types: Distinguish subclasses in hash-based collections
  2. Type hierarchies: Include superclass hash in total hash
  3. Hash distribution: Add constant factors for better hash spread
  4. Custom hashing logic: Integrate with existing hashing infrastructure

Best practice:

For most use cases, the default hash code is sufficient. Only use includeHashCode when:

  • You have inheritance hierarchies and need type discrimination
  • You’re integrating with existing hash code implementations
  • You have specific requirements for hash distribution

See also: underrideHashCode, prehash, lazyhash


init

Default: "*"

Builder initialization method. i.e. “setter” in builder. Do not confuse with set

Naming template for builder initialization methods (i.e., “setters” in builder). This controls how attribute values are set when constructing immutable objects via builders.

Default behavior (init = ”*”):

@Value.Immutable
interface Person {
  String name();
  int age();
}

// Generated builder methods use attribute name as-is:
ImmutablePerson person = ImmutablePerson.builder()
    .name("Alice")   // init template: "*" → "name"
    .age(30)          // init template: "*" → "age"
    .build();

Use “set”:*

@Value.Style(init = "set*")
@Value.Immutable
interface Person {
  String name();
  int age();
}

// Generated builder methods have "set" prefix:
ImmutablePerson person = ImmutablePerson.builder()
    .setName("Alice")  // init template: "set*" → "setName"
    .setAge(30)         // init template: "set*" → "setAge"
    .build();

Use “with”:*

@Value.Style(init = "with*")
@Value.Immutable
interface Config {
  String host();
  int port();
}

// Generated builder methods have "with" prefix:
Config config = ImmutableConfig.builder()
    .withHost("localhost")  // "with*" → "withHost"
    .withPort(8080)         // "with*" → "withPort"
    .build();

Difference between init, set, and with:

AttributeUsed ForDefaultExample
initBuilder initialization methods"*"builder.name("Alice") or builder.setName("Alice")
setModifiable “setter” methods"set*"modifiable.setName("Alice")
withImmutable copy-with methods"with*"immutable.withName("Alice")
@Value.Style(
  init = "set*",   // Builder: .setName()
  set = "put*",    // Modifiable: .putName()
  with = "copyWith*"   // Immutable withers: .withName()
)
@Value.Immutable
@Value.Modifiable
interface Person {
  String name();
}

// Builder uses 'init':
ImmutablePerson.builder().setName("Alice").build();

// Modifiable uses 'set':
ModifiablePerson modifiable = ModifiablePerson.create();
modifiable.putName("Alice");

// Immutable wither uses 'with':
ImmutablePerson updated = person.copyWithName("Bob");

Interaction with get pattern:

@Value.Style(
  get = {"is*", "get*"},  // Detection: strip "is"/"get" from accessors
  init = "set*"            // Generation: add "set" prefix to builder methods
)
@Value.Immutable
interface Config {
  String getName();      // Detected as attribute "name"
  boolean isEnabled();   // Detected as attribute "enabled"
}

// Generated builder:
ImmutableConfig.builder()
  .setName("test")     // "set" + "name"
  .setEnabled(true)    // "set" + "enabled"
  .build();

Collection attributes:

For collection attributes, init affects the base setter, while specific collection operations have their own templates:

@Value.Style(init = "set*")
@Value.Immutable
interface Data {
  List<String> items();
}

// Generated methods:
ImmutableData.builder()
  .setItems(List.of("a", "b"))  // init: "set*"
  .addItems("c")                 // add: "add*" (separate template)
  .addAllItems(List.of("d", "e")) // addAll: "addAll*" (separate template)
  .build();

Use cases:

  1. JavaBean compatibility: Use init = "set*" for frameworks expecting JavaBean patterns
  2. Consistency: Match existing codebase conventions
  3. Migration: Gradually adopt Immutables by matching current naming

Best practices:

  1. Apply at package level: Use meta-annotation or package-info.java for consistency
  2. Match conventions: Use init = "set*" if your team uses set style
  3. Keep it simple: Default "*" works well for most cases
  4. Document your choice: Make it clear in team guidelines

Meta-annotation example:

@Value.Style(
  get = {"is*", "get*"},
  init = "set*",
  with = "with*"
)
@Target({ElementType.PACKAGE, ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface BeanStyle {}

// Apply to all types in package:
@BeanStyle
package com.example.models;

See also: set, with, get, add


instance

Default: "of"

Singleton accessor method name. Naming template for the singleton accessor method when @Value.Immutable(singleton = true) is used.

Default behavior (instance = “of”):

@Value.Immutable(singleton = true)
interface Constants {
  @Value.Default
  default String apiVersion() {
    return "v1";
  }

  @Value.Default
  default int timeout() {
    return 30;
  }
}

// Generated singleton accessor:
Constants constants = ImmutableConstants.of();  // "of" is the method name

Custom naming:

@Value.Style(instance = "getInstance")
@Value.Immutable(singleton = true)
interface GlobalConfig {
  @Value.Default
  default String environment() {
    return "production";
  }
}

// Generated singleton accessor uses custom name:
GlobalConfig config = ImmutableGlobalConfig.getInstance();

Other naming examples:

// Pattern 1: getInstance (classic singleton pattern)
@Value.Style(instance = "getInstance")
// Usage: ImmutableConfig.getInstance()

// Pattern 2: instance (field-style)
@Value.Style(instance = "instance")
// Usage: ImmutableConfig.instance()

// Pattern 3: get (simple getter)
@Value.Style(instance = "get")
// Usage: ImmutableConfig.get()

// Pattern 4: shared (descriptor name)
@Value.Style(instance = "shared")
// Usage: ImmutableConfig.shared()

// Pattern 5: of (default - factory style)
@Value.Style(instance = "of")
// Usage: ImmutableConfig.of()

Singleton pattern implementation:

@Value.Style(instance = "getInstance")
@Value.Immutable(singleton = true)
interface AppConfig {
  @Value.Default
  default String appName() {
    return "MyApp";
  }
}

// Usage:
AppConfig config = ImmutableAppConfig.getInstance();
String name = config.appName();  // "MyApp"

Use cases:

  1. Application constants: Global configuration values
  2. Empty collections: Singleton for empty immutable collections
  3. Default configurations: Shared default settings
  4. Stateless utilities: Immutable utility objects

Example: Application constants:

@Value.Style(instance = "constants")
@Value.Immutable(singleton = true)
interface HttpConstants {
  @Value.Default
  default int defaultPort() {
    return 80;
  }

  @Value.Default
  default int defaultTimeout() {
    return 30_000;
  }

  @Value.Default
  default String defaultUserAgent() {
    return "MyApp/1.0";
  }
}

// Usage everywhere:
int port = ImmutableHttpConstants.constants().defaultPort();

Example: Empty collection-like singleton:

@Value.Style(instance = "empty")
@Value.Immutable(singleton = true)
interface EmptyList<T> {
  @Value.Default
  default List<T> items() {
    return Collections.emptyList();
  }
}

// Usage:
EmptyList<?> empty = ImmutableEmptyList.empty();

Interaction with of template:

Note that instance is separate from of but happen to have the same default value "of":

  • instance: Singleton accessor (no parameters)
  • of: Static factory method with constructor parameters
// instance is for singletons:
@Value.Immutable(singleton = true)
interface Singleton {
  @Value.Default
  default String value() { return "x"; }
}
Singleton s = ImmutableSingleton.of();  // Uses 'instance' template

// of is for constructor methods:
@Value.Immutable
interface Data {
  @Value.Parameter String value();
}
Data d = ImmutableData.of("x");  // Uses 'of' template

See also: singleton, of, builder


isInitialized

Default: "isInitialized"

Applies to: @Value.Modifiable types only

Naming template for a method on modifiable objects that checks if all mandatory (required) attributes have been set. This method determines if the modifiable instance is ready to be converted to an immutable instance via toImmutable().

What is this method?

This method is generated on @Value.Modifiable companion types to check if all required attributes are set. It returns true when the modifiable object has all mandatory attributes initialized and false otherwise.

Default behavior (isInitialized = “isInitialized”):

@Value.Immutable
@Value.Modifiable
interface Person {
  String name();      // Required
  int age();          // Required
  Optional<String> nickname();  // Optional
}

// Generated on ModifiablePerson:
ModifiablePerson person = ModifiablePerson.create();
person.isInitialized();  // false - nothing set yet

person.setName("Alice");
person.isInitialized();  // false - age still missing

person.setAge(30);
person.isInitialized();  // true - all required fields set

// Can now convert to immutable
ImmutablePerson immutable = person.toImmutable();

Custom naming:

@Value.Style(isInitialized = "isReady")
@Value.Immutable
@Value.Modifiable
interface Config {
  String host();
  int port();
}

ModifiableConfig config = ModifiableConfig.create();
config.isReady();  // Custom method name

When is isInitialized true?

The method returns true when:

  • All required attributes (not counting any @Nullable, Optional or @Value.Default or collections) are set

Example: Safe conversion to immutable:

@Value.Immutable
@Value.Modifiable
interface User {
  String username();
  String email();
  Optional<String> displayName();
}

ModifiableUser user = ModifiableUser.create();

user.setUsername("alice");
if (user.isInitialized()) {
  // false - email still missing
}

user.setEmail("alice@example.com");
if (user.isInitialized()) {
  // true - all required fields set
  ImmutableUser immutable = user.toImmutable();
}

Interaction with clear() and unset():*

ModifiableCompanion c = ModifiableCompanion.create();
c.setString("value");
c.setInteger(42);

c.isInitialized();  // true

c.clear();  // Clears collections and unsets optional attributes

c.isInitialized();  // false - mandatory attributes unset

Distinction from canBuild on builders:

isInitialized() on modifiables is conceptually similar to canBuild() on builders, but they apply to different types:

  • isInitialized(): Method on @Value.Modifiable types - checks if modifiable can be converted to immutable via toImmutable()
  • canBuild(): Method on builders - checks if builder can produce an immutable instance via build()

Both serve the same purpose (checking readiness) but for different mutable wrapper types.

Example comparing both:

@Value.Immutable
@Value.Modifiable
@Value.Style(canBuild = "canBuild")
interface Data {
  String value();
}

// On modifiable:
ModifiableData mod = ModifiableData.create();
mod.isInitialized();  // false
mod.setValue("test");
mod.isInitialized();  // true
mod.toImmutable();    // succeeds

// On builder:
ImmutableData.Builder builder = ImmutableData.builder();
builder.canBuild();   // false
builder.value("test");
builder.canBuild();   // true
builder.build();      // succeeds

See also: canBuild, isSet, clear, Modifiable instances


isSet

Default: "*IsSet"

Naming template for methods that check if an individual attribute has been set. Used on Modifiable types and internally on builders. In order to expose it on builders (make it public, or package-private if attribute itself is package-private), use isSetOnBuilder() style flag.

*Default behavior (isSet = “IsSet”):

@Value.Immutable
@Value.Modifiable
interface Config {
  String host();
  int port();
  Optional<String> description();
}

// Generated on ModifiableConfig:
ModifiableConfig config = ModifiableConfig.create();

config.hostIsSet();  // false
config.setHost("localhost");
config.hostIsSet();  // true

config.portIsSet();  // false
config.setPort(8080);
config.portIsSet();  // true

config.descriptionIsSet();  // false (Optional not set)

Custom naming:

@Value.Style(isSet = "has*")
@Value.Immutable
@Value.Modifiable
interface Data {
  String value();
}

// Generated on ModifiableData:
ModifiableData data = ModifiableData.create();
data.hasValue();  // false
data.setValue("test");
data.hasValue();  // true

Common naming patterns:

// Pattern 1: *IsSet (default)
@Value.Style(isSet = "*IsSet")
// Usage: config.hostIsSet()

// Pattern 2: has* (common alternative)
@Value.Style(isSet = "has*")
// Usage: config.hasHost()

// Pattern 3: is*Set
@Value.Style(isSet = "is*Set")
// Usage: config.isHostSet()

// Pattern 4: check*
@Value.Style(isSet = "check*")
// Usage: config.checkHost()

// Pattern 5: is*Initialized
@Value.Style(isSet = "is*Initialized")
// Usage: config.isHostInitialized()

Use on Modifiables:

The primary use case is with @Value.Modifiable:

@Value.Style(isSet = "has*")
@Value.Immutable
@Value.Modifiable
interface UserProfile {
  String username();
  @Nullable String email();
  @Nullable String phone();
}

// Usage with Modifiable:
ModifiableUserProfile profile = ModifiableUserProfile.create();

// Check what's set:
if (!profile.hasUsername()) {
  profile.setUsername("alice");
}

if (!profile.hasEmail()) {
  profile.setEmail("alice@example.com");
}

// Only phone was not set:
assert profile.hasUsername();
assert profile.hasEmail();
assert !profile.hasPhone();

Use on builders (internal):

By default, isSet methods on builders are internal (package-private or private). To make them public, use isSetOnBuilder = true:

// Default: isSet methods are internal on builders
@Value.Immutable
interface Data {
  String value();
}

ImmutableData.Builder builder = ImmutableData.builder();
// builder.valueIsSet()  // Not accessible (internal)

// Make public with isSetOnBuilder:
@Value.Style(isSetOnBuilder = true, isSet = "*IsSet")
@Value.Immutable
interface Data {
  String value();
}

ImmutableData.Builder builder = ImmutableData.builder();
builder.valueIsSet();  // Now public! Returns false
builder.value("test");
builder.valueIsSet();  // Returns true

Behavior with different attribute types:

@Value.Style(isSet = "has*")
@Value.Immutable
@Value.Modifiable
interface AllTypes {
  String required();           // has*() checks if set
  @Nullable String nullable(); // has*() checks if set (even to null)
  Optional<String> optional(); // has*() checks if set
  @Value.Default
  default String withDefault() {
    return "default";
  }
}

ModifiableAllTypes obj = ModifiableAllTypes.create();

// Required:
obj.hasRequired();  // false
obj.setRequired("value");
obj.hasRequired();  // true

// Nullable:
obj.hasNullable();  // false
obj.setNullable(null);  // Set to null
obj.hasNullable();  // true (set, even though null!)

// Optional:
obj.hasOptional();  // false
obj.setOptional(Optional.empty());
obj.hasOptional();  // true (set to empty)

// Default:
obj.hasWithDefault();  // true (has default value)

Important: Setting to null still means “set”:

@Value.Modifiable
@Value.Immutable
interface Data {
  @Nullable String value();
}

ModifiableData data = ModifiableData.create();
data.valueIsSet();  // false

data.setValue(null);  // Set to null
data.valueIsSet();  // true! (it WAS set, even to null)

Use cases:

  1. Modifiable state tracking: Know which fields have been modified
  2. Partial updates: Only update fields that were explicitly set
  3. Form validation: Check if required fields were filled
  4. Conditional logic: Execute code based on whether fields are set
  5. Debugging: Inspect builder/modifiable state

Example: Partial updates:

@Value.Style(isSet = "was*Changed")
@Value.Immutable
@Value.Modifiable
interface UserUpdate {
  @Nullable String email();
  @Nullable String phone();
  @Nullable String address();
}

void updateUser(User user, ModifiableUserUpdate changes) {
  if (changes.wasEmailChanged()) {
    user.setEmail(changes.getEmail());
  }
  if (changes.wasPhoneChanged()) {
    user.setPhone(changes.getPhone());
  }
  if (changes.wasAddressChanged()) {
    user.setAddress(changes.getAddress());
  }
}

Example: Form validation:

@Value.Style(isSet = "has*")
@Value.Modifiable
@Value.Immutable
interface RegistrationForm {
  String username();
  String password();
  String email();
}

boolean validateForm(ModifiableRegistrationForm form) {
  return form.hasUsername()
      && form.hasPassword()
      && form.hasEmail();
}

Interaction with unset methods:

On modifiables, you can also unset attributes:

@Value.Style(isSet = "has*", unset = "clear*")
@Value.Modifiable
@Value.Immutable
interface Data {
  @Nullable String value();
}

ModifiableData data = ModifiableData.create();
data.hasValue();  // false

data.setValue("test");
data.hasValue();  // true

data.clearValue();  // Unset
data.hasValue();  // false again

See also: isSetOnBuilder, unset, set, @Value.Modifiable


isSetOnBuilder

Default: false

When true, builder will have isSet methods generated for each attribute, like aIsSet(), bIsSet(). These are telling if attribute was initialized on a builder. The actual naming template is configured by isSet style attribute.

Default behavior (isSetOnBuilder = false):

@Value.Immutable
interface Config {
  String host();
  int port();
}

ImmutableConfig.Builder builder = ImmutableConfig.builder();
// builder.hostIsSet()  // Method exists but is INTERNAL (not public)

With isSetOnBuilder = true:

@Value.Style(
  isSetOnBuilder = true,
  isSet = "*IsSet"
)
@Value.Immutable
interface Config {
  String host();
  int port();
  Optional<String> description();
}

// Generated public methods on builder:
ImmutableConfig.Builder builder = ImmutableConfig.builder();

builder.hostIsSet();  // false - not set yet
builder.portIsSet();  // false
builder.descriptionIsSet();  // false

builder.host("localhost");
builder.hostIsSet();  // true - now set
builder.portIsSet();  // false - still not set

builder.port(8080);
builder.portIsSet();  // true

// Optional not set, but that's OK:
builder.descriptionIsSet();  // false

Config config = builder.build();

Use cases:

  1. Conditional building: Check field state before building
  2. Form validation: Verify required fields are filled
  3. Progressive disclosure: Show/hide fields based on others
  4. Builder state inspection: Debug what’s been set
  5. Default value detection: Know if user provided value or using default

Example: Form validation:

@Value.Style(isSetOnBuilder = true, isSet = "has*")
@Value.Immutable
interface RegistrationData {
  String username();
  String password();
  String email();
  Optional<String> phoneNumber();
}

ImmutableRegistrationData.Builder form = ImmutableRegistrationData.builder();

void validateAndSubmit() {
  // Check all required fields:
  if (!form.hasUsername()) {
    showError("Username is required");
    return;
  }
  if (!form.hasPassword()) {
    showError("Password is required");
    return;
  }
  if (!form.hasEmail()) {
    showError("Email is required");
    return;
  }

  // phoneNumber is optional, OK if not set
  RegistrationData data = form.build();
  submit(data);
}

Example: Progressive UI:

@Value.Style(isSetOnBuilder = true)
@Value.Immutable
interface ShippingAddress {
  String country();
  String state();     // Only for certain countries
  String zipCode();
}

ImmutableShippingAddress.Builder builder = ImmutableShippingAddress.builder();

void onCountrySelected(String country) {
  builder.country(country);

  if ("USA".equals(country) || "Canada".equals(country)) {
    showStateField();  // Show state dropdown
  } else {
    hideStateField();  // Hide for other countries
  }
}

void validateForm() {
  if (!builder.countryIsSet()) {
    return; // Can't validate yet
  }

  String country = builder.build().country();  // Safe to build for just this field
  if (("USA".equals(country) || "Canada".equals(country)) && !builder.stateIsSet()) {
    showError("State is required for " + country);
  }
}

Example: Detecting user-provided values:

@Value.Style(isSetOnBuilder = true)
@Value.Immutable
interface Configuration {
  @Value.Default
  default int timeout() {
    return 30;
  }

  @Value.Default
  default String host() {
    return "localhost";
  }
}

ImmutableConfiguration.Builder builder = ImmutableConfiguration.builder();

// User explicitly sets timeout:
builder.timeout(60);

// Don't set host - will use default

// Now we can tell which were explicitly set:
if (builder.timeoutIsSet()) {
  log("User provided custom timeout: " + builder.build().timeout());
} else {
  log("Using default timeout");
}

if (builder.hostIsSet()) {
  log("User provided custom host");
} else {
  log("Using default host");  // This will log
}

Behavior with different attribute types:

@Value.Style(isSetOnBuilder = true, isSet = "is*Set")
@Value.Immutable
interface Data {
  String required();
  @Nullable String nullable();
  Optional<String> optional();
  @Value.Default
  default String withDefault() {
    return "default";
  }
}

ImmutableData.Builder builder = ImmutableData.builder();

// Required:
builder.isRequiredSet();  // false
builder.required("value");
builder.isRequiredSet();  // true

// Nullable:
builder.isNullableSet();  // false
builder.nullable(null);    // Set to null
builder.isNullableSet();  // true (was explicitly set!)

// Optional:
builder.isOptionalSet();  // false
builder.optional(Optional.empty());
builder.isOptionalSet();  // true (set to empty)

// Default:
builder.isWithDefaultSet();  // false (not set, will use default)
builder.withDefault("custom");
builder.isWithDefaultSet();  // true (explicitly set)

Interaction with clearBuilder:

When using clearBuilder = true, you can reset the builder and check state again:

@Value.Style(
  clearBuilder = true,
  isSetOnBuilder = true,
  isSet = "*IsSet",
  clear = "reset"
)
@Value.Immutable
interface Data {
  String value();
}

ImmutableData.Builder builder = ImmutableData.builder();

builder.value("test");
builder.valueIsSet();  // true

builder.reset();  // Clear all fields
builder.valueIsSet();  // false (cleared!)

Interaction with strictBuilder:

With strictBuilder = true, isSet methods help avoid double-initialization:

@Value.Style(
  strictBuilder = true,
  isSetOnBuilder = true
)
@Value.Immutable
interface StrictData {
  String value();
}

ImmutableStrictData.Builder builder = ImmutableStrictData.builder();

if (!builder.valueIsSet()) {
  builder.value("test");
}

// Trying to set again would throw:
if (!builder.valueIsSet()) {
  builder.value("other");  // Won't execute
}

Performance consideration:

There’s a small memory overhead for tracking which fields are set. For most use cases, this is negligible, but for builders with many attributes in memory-constrained environments, consider leaving this disabled.

When to enable:

  • ✓ Building UIs with form validation
  • ✓ Need to distinguish “not set” from “set to default value”
  • ✓ Implementing conditional logic based on field state
  • ✓ Debugging complex builder scenarios

When to leave disabled (default):

  • ✓ Simple builders with no state inspection needs
  • ✓ Performance-critical code with tight memory constraints
  • ✓ Builders used in straightforward, linear construction patterns

See also: isSet, canBuild, clearBuilder, strictBuilder


jacksonIntegration

Type: boolean Default: true

Controls whether special Jackson integration code is generated when @JsonSerialize/@JsonDeserialize annotations are detected on value types.

Javadoc

From org.immutables.value.Value.Style.jacksonIntegration():

Setting this to false would disable any special jackson integration capabilities. While out-of-the-box Jackson readiness is a good things in the most cases, for some cases it might get in the way of highly customized Jackson infrastructure. When disabled, there are no any special stuff generated such as JsonProperty annotations or internal Json delegate class together with JsonCreator method. This allows to place JsonSerialize/JsonDeserialize annotations on the value types without redundant support code being generated.

What gets generated when enabled

When jacksonIntegration = true (default) and Jackson annotations are present:

  1. @JsonProperty annotations on all attributes in the implementation class
  2. Internal Json delegate class for deserialization
  3. @JsonCreator factory method for Jackson to call
  4. Proper handling of @JsonIgnore, @JsonInclude, etc.

Default behavior (jacksonIntegration = true)

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

@Value.Immutable
@JsonSerialize(as = ImmutablePerson.class)
@JsonDeserialize(as = ImmutablePerson.class)
interface Person {
  String name();
  int age();
}

// Generated implementation includes:
final class ImmutablePerson implements Person {
  @JsonProperty("name")  // Generated
  private final String name;

  @JsonProperty("age")   // Generated
  private final int age;

  @JsonCreator           // Generated
  static ImmutablePerson fromJson(
      @JsonProperty("name") String name,
      @JsonProperty("age") int age) {
    return ImmutablePerson.builder()
      .name(name)
      .age(age)
      .build();
  }

  // Internal Json delegate class also generated
  static final class Json {
    // Jackson deserialization support
  }
}

Customized behavior (jacksonIntegration = false)

@Value.Style(jacksonIntegration = false)
@Value.Immutable
@JsonSerialize(as = ImmutablePerson.class)
@JsonDeserialize(builder = ImmutablePerson.Builder.class)
interface Person {
  String name();
  int age();

  @JsonIgnore
  default String displayName() {
    return name() + " (" + age() + ")";
  }
}

// Generated implementation WITHOUT Jackson integration:
final class ImmutablePerson implements Person {
  private final String name;  // NO @JsonProperty
  private final int age;      // NO @JsonProperty

  // NO @JsonCreator method
  // NO Json delegate class

  // You provide your own Jackson configuration:
  @JsonPOJOBuilder(withPrefix = "")
  public static final class Builder {
    // Your custom Jackson setup
  }
}

When to disable jacksonIntegration

Use jacksonIntegration = false when:

  1. Custom Jackson mixins: You’re using Jackson mixins for serialization configuration
  2. Custom deserializers: You’ve written custom JsonDeserializer implementations
  3. Alternative JSON libraries: Using Gson, Moshi, or other JSON libraries
  4. Fine-grained control: Need precise control over JSON property names and behavior
  5. Avoiding conflicts: Generated Jackson code conflicts with your custom setup

Example: Custom Jackson configuration:

@Value.Style(jacksonIntegration = false)
@Value.Immutable
@JsonSerialize(as = ImmutableConfig.class)
@JsonDeserialize(using = ConfigDeserializer.class)  // Custom deserializer
interface Config {
  String apiKey();
  String apiSecret();
}

// Your custom deserializer:
public class ConfigDeserializer extends JsonDeserializer<Config> {
  @Override
  public Config deserialize(JsonParser p, DeserializationContext ctx)
      throws IOException {
    // Custom deserialization logic
    JsonNode node = p.getCodec().readTree(p);
    return ImmutableConfig.builder()
      .apiKey(decrypt(node.get("key").asText()))
      .apiSecret(decrypt(node.get("secret").asText()))
      .build();
  }
}

Interaction with other Jackson style attributes

@Value.Style(
  jacksonIntegration = true,          // Enable Jackson support
  forceJacksonPropertyNames = true,   // Force @JsonProperty even when names match
  setJacksonPropertyRequired = true,  // Mark required fields in @JsonProperty
  depluralize = true                  // Affects collection property names
)
@Value.Immutable
interface Order {
  @JsonProperty("order_id")
  String id();

  List<String> items();  // Will have @JsonProperty("items")
}

Performance considerations

Enabled (default):

  • Adds 5-10KB per immutable class (internal Json delegate)
  • Slightly slower compilation due to additional code generation
  • Runtime performance identical (Jackson uses generated code)

Disabled:

  • Smaller generated files
  • Faster compilation
  • You must provide Jackson configuration manually

Common mistakes

Mistake: Disabling without providing alternative:

@Value.Style(jacksonIntegration = false)
@Value.Immutable
@JsonSerialize(as = ImmutableData.class)
@JsonDeserialize(as = ImmutableData.class)  // Won't work!
interface Data {
  String value();
}

// Deserialization will fail - no @JsonCreator or builder config

Fix: Provide builder configuration:

@Value.Style(jacksonIntegration = false)
@Value.Immutable
@JsonSerialize(as = ImmutableData.class)
@JsonDeserialize(builder = ImmutableData.Builder.class)
interface Data {
  String value();

  @JsonPOJOBuilder(withPrefix = "")
  static class Builder extends ImmutableData.Builder {}
}

See also: forceJacksonPropertyNames, setJacksonPropertyRequired, additionalJsonAnnotations


jakarta

Type: boolean Default: false

When enabled, jakarta.* packages take precedence over javax.* packages during annotation discovery and code generation. Primarily affects jakarta.annotation.* and jakarta.validation.*.

Javadoc

From org.immutables.value.Value.Style.jakarta():

If enabled, jakarta.* packages will take over any relevant javax.*. This includes primarily jakarta.annotation.* and jakarta.validation.*. Note that classpath inhibitor or allowedClasspathAnnotations will still take effect, it’s just so that

Background: javax to jakarta migration

With Java EE transitioning to Jakarta EE (under Eclipse Foundation), many packages were renamed from javax.* to jakarta.*:

  • javax.annotation.Generatedjakarta.annotation.Generated
  • javax.annotation.Nullablejakarta.annotation.Nullable
  • javax.validation.constraints.*jakarta.validation.constraints.*

This style attribute lets you control which namespace Immutables uses when generating code.

Default behavior (jakarta = false)

@Value.Immutable
interface Person {
  @javax.validation.constraints.NotNull
  String name();

  @javax.annotation.Nullable
  String nickname();
}

// Generated code uses javax:
@javax.annotation.Generated("org.immutables.processor.ProxyProcessor")
final class ImmutablePerson implements Person {
  @javax.annotation.Nullable
  private final String nickname;

  // Validation uses javax.validation if enabled
}

Jakarta-enabled behavior (jakarta = true)

@Value.Style(jakarta = true)
@Value.Immutable
interface Person {
  @jakarta.validation.constraints.NotNull
  String name();

  @jakarta.annotation.Nullable
  String nickname();
}

// Generated code uses jakarta:
@jakarta.annotation.Generated("org.immutables.processor.ProxyProcessor")
final class ImmutablePerson implements Person {
  @jakarta.annotation.Nullable
  private final String nickname;

  // Validation uses jakarta.validation if enabled
}

When to enable jakarta

Enable jakarta = true when:

  1. Using Jakarta EE 9+: Your application uses Jakarta EE 9 or later
  2. Spring Boot 3+: Spring Boot 3.0+ uses jakarta namespace
  3. Modern Java frameworks: Most new frameworks use jakarta packages
  4. Migration projects: Transitioning from javax to jakarta

Keep jakarta = false when:

  1. Legacy applications: Java EE 8 or earlier
  2. Spring Boot 2.x: Still uses javax namespace
  3. Mixed dependencies: Some dependencies haven’t migrated to jakarta

Example: Spring Boot 3 application

@Value.Style(jakarta = true)  // Spring Boot 3 uses jakarta
package com.example.model;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Email;

@Value.Immutable
interface UserRegistration {
  @NotNull
  String username();

  @NotNull
  @Email
  String email();

  @NotNull
  String password();
}

// Generated validation uses jakarta.validation:
final class ImmutableUserRegistration {
  private void validate() {
    // Uses jakarta.validation.Validator
  }
}

Interaction with allowedClasspathAnnotations

The jakarta setting works together with allowedClasspathAnnotations:

@Value.Style(
  jakarta = true,
  allowedClasspathAnnotations = {
    jakarta.validation.constraints.NotNull.class,
    jakarta.annotation.Nullable.class
  }
)
@Value.Immutable
interface Data {
  @jakarta.validation.constraints.NotNull
  String value();
}

// Only specified jakarta annotations are recognized

Affected annotations

Annotations affected by jakarta = true:

javax namespacejakarta namespace
javax.annotation.Generatedjakarta.annotation.Generated
javax.annotation.Nullablejakarta.annotation.Nullable
javax.validation.constraints.*jakarta.validation.constraints.*
javax.validation.Validjakarta.validation.Valid

Migration example

Before (javax):

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.annotation.Nullable;

@Value.Immutable
interface Account {
  @NotNull String id();
  @Min(0) int balance();
  @Nullable String notes();
}

After (jakarta):

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
import jakarta.annotation.Nullable;

@Value.Style(jakarta = true)
@Value.Immutable
interface Account {
  @NotNull String id();
  @Min(0) int balance();
  @Nullable String notes();
}

Common migration scenarios

Scenario 1: Mixed dependencies during migration

// Some dependencies still use javax, others use jakarta
@Value.Style(
  jakarta = true,  // Prefer jakarta
  // But allow both during migration
  allowedClasspathAnnotations = {
    jakarta.validation.constraints.NotNull.class,
    javax.validation.constraints.NotNull.class  // Backwards compat
  }
)

Scenario 2: Package-level configuration

@Value.Style(jakarta = true)
package com.example.model;

// All value types in this package use jakarta

Source file reference

Example (value-fixture/src/org/immutables/fixture/style/LostInJakarta.java:21):

@Value.Style(jakarta = true)

Important notes

  • The jakarta setting only affects auto-discovered annotations from classpath
  • allowedClasspathAnnotations and classpath inhibitor still take effect
  • Does not automatically convert annotations in your source code (you must update imports)
  • Only affects which namespace Immutables generates in implementation code

See also: allowedClasspathAnnotations, nullableAnnotation, fallbackNullableAnnotation


jdk9Collections

Type: boolean Default: false

When enabled, uses JDK 9+ immutable collection factory methods (List.of(), Set.copyOf(), Map.of()) instead of Guava or custom implementations for List/Set/Map attributes.

Javadoc

From org.immutables.value.Value.Style.jdk9Collections():

When true — will use JDK 9+ immutable collections to implement List/Set/Map attributes. In JDK 9+, immutable collections are instantiated via of/copyOf static methods on List, Set, Map interfaces: List.of(), Set.copyOf(Collection), etc. Please note that these collections do not support null elements, also Sets and Maps do not maintain insertion order, so the order is arbitrary and cannot be relied upon.

Default behavior (jdk9Collections = false)

@Value.Immutable
interface ShoppingCart {
  List<String> items();
  Set<String> categories();
  Map<String, Integer> quantities();
}

// Generated code uses Guava (if available) or fallback:
final class ImmutableShoppingCart {
  private final ImmutableList<String> items;      // Guava
  private final ImmutableSet<String> categories;  // Guava
  private final ImmutableMap<String, Integer> quantities;  // Guava

  // Factory methods use Guava builders
}

JDK 9+ collections behavior (jdk9Collections = true)

@Value.Style(jdk9Collections = true)
@Value.Immutable
interface ShoppingCart {
  List<String> items();
  Set<String> categories();
  Map<String, Integer> quantities();
}

// Generated code uses JDK 9+ collections:
final class ImmutableShoppingCart {
  private final List<String> items;      // java.util.List
  private final Set<String> categories;  // java.util.Set
  private final Map<String, Integer> quantities;  // java.util.Map

  static ImmutableShoppingCart.Builder builder() {
    return new Builder();
  }

  public static class Builder {
    public ImmutableShoppingCart build() {
      return new ImmutableShoppingCart(
        List.copyOf(items),           // JDK 9+ List.copyOf()
        Set.copyOf(categories),       // JDK 9+ Set.copyOf()
        Map.copyOf(quantities)        // JDK 9+ Map.copyOf()
      );
    }
  }
}

Critical limitations

1. No null elements allowed:

@Value.Style(jdk9Collections = true)
@Value.Immutable
interface Data {
  List<String> values();
}

// This will throw NullPointerException:
ImmutableData data = ImmutableData.builder()
  .addValues("a", null, "c")  // NPE! JDK 9+ collections reject nulls
  .build();

2. Sets and Maps have arbitrary order:

@Value.Style(jdk9Collections = true)
@Value.Immutable
interface Config {
  Set<String> features();
}

ImmutableConfig config = ImmutableConfig.builder()
  .addFeatures("feature1", "feature2", "feature3")
  .build();

// Iteration order is NOT guaranteed:
config.features();  // May be ["feature3", "feature1", "feature2"]

// DON'T rely on insertion order with jdk9Collections + Set/Map

3. Lists maintain order (safe to use):

@Value.Style(jdk9Collections = true)
@Value.Immutable
interface OrderedData {
  List<String> items();  // Safe - Lists maintain order
}

ImmutableOrderedData data = ImmutableOrderedData.builder()
  .addItems("first", "second", "third")
  .build();

data.items();  // Guaranteed: ["first", "second", "third"]

When to enable jdk9Collections

Enable when:

  1. No Guava dependency: Want to avoid Guava for smaller deployment size
  2. JDK 11+ minimum: Target JDK 11 or later
  3. No nulls: Your data model never uses null collection elements
  4. Set/Map order doesn’t matter: Don’t rely on insertion order for sets/maps

Don’t enable when:

  1. Need null support: Any collection attributes may contain null elements
  2. Need insertion-order Sets: Rely on LinkedHashSet behavior
  3. Need insertion-order Maps: Rely on LinkedHashMap behavior
  4. Target JDK 8: JDK 9+ collections not available

Comparison table

FeatureGuava (default)JDK 9+ Collections
Null elements✗ Rejected✗ Rejected
List ordering✓ Maintained✓ Maintained
Set ordering✓ Insertion orderArbitrary
Map ordering✓ Insertion orderArbitrary
DependencyGuava requiredJDK 9+ only
MemorySlightly higherSlightly lower
PerformanceVery fastVery fast

Interaction with jdkOnly

@Value.Style(
  jdkOnly = true,         // Don't use Guava
  jdk9Collections = true  // Use JDK 9+ collections
)
@Value.Immutable
interface Data {
  List<String> items();
}

// Uses JDK 9+ collections, no Guava dependency

Without jdk9Collections, jdkOnly = true would use custom fallback implementations instead of Guava.

Example: Avoiding Guava dependency

@Value.Style(jdk9Collections = true)
package com.example.model;

// No Guava dependency needed

@Value.Immutable
interface ApiResponse {
  int statusCode();
  List<String> headers();  // JDK 9+ List
  Map<String, String> metadata();  // JDK 9+ Map (UNORDERED!)
}

// pom.xml - No Guava:
// <dependencies>
//   <!-- No com.google.guava:guava needed -->
// </dependencies>

Common mistakes

Mistake: Relying on Set iteration order:

@Value.Style(jdk9Collections = true)
@Value.Immutable
interface Report {
  Set<String> sections();  // Order NOT guaranteed!
}

ImmutableReport report = ImmutableReport.builder()
  .addSections("Introduction", "Body", "Conclusion")
  .build();

// Printing sections may be out of order:
report.sections().forEach(System.out::println);
// May output: "Body", "Conclusion", "Introduction"

Fix: Use List for ordered collections:

@Value.Style(jdk9Collections = true)
@Value.Immutable
interface Report {
  List<String> sections();  // Order guaranteed
}

ImmutableReport report = ImmutableReport.builder()
  .addSections("Introduction", "Body", "Conclusion")
  .build();

report.sections().forEach(System.out::println);
// Always outputs: "Introduction", "Body", "Conclusion"

Runtime requirements

Minimum JDK version:

  • JDK 9+ for List.of(), Set.of(), Map.of()
  • JDK 10+ for List.copyOf(), Set.copyOf(), Map.copyOf()

If using JDK 9 exactly, some methods may use of() instead of copyOf().

Performance characteristics

JDK 9+ collections are highly optimized:

  • Small collections (0-2 elements): Specialized implementations
  • Larger collections: Array-based implementations
  • Memory efficient: No overhead from builder classes
  • Immutable: True compile-time immutability guarantees

Memory comparison (1000-element List):

  • Guava ImmutableList: ~8KB + collection overhead
  • JDK 9+ List.copyOf(): ~8KB (slightly more compact)

The difference is minimal but can add up in large applications.

See also: jdkOnly, builtinContainerAttributes


jdkOnly

Type: boolean Default: false

Forces generation of code using only JDK 7+ standard library classes, completely avoiding Google Guava dependencies.

Javadoc

From org.immutables.value.Value.Style.jdkOnly():

When true — forces to generate code which use only JDK 7+ standard library classes. It is false by default, however usage of JDK-only classes will be turned on automatically if Google Guava library is not found in classpath. The generated code will have subtle differences, but nevertheless will be functionally equivalent.

Note that some additional annotation processors (for example mongo repository generator) may not work without Guava being accessible to the generated classes, and thus will not honor this attribute

Default behavior (jdkOnly = false)

@Value.Immutable
interface Product {
  String name();
  List<String> tags();
  Optional<String> description();
}

// Generated code uses Guava when available:
import com.google.common.collect.ImmutableList;
import com.google.common.base.Optional;

final class ImmutableProduct {
  private final ImmutableList<String> tags;  // Guava
  private final Optional<String> description;  // Guava Optional
}

JDK-only behavior (jdkOnly = true)

@Value.Style(jdkOnly = true)
@Value.Immutable
interface Product {
  String name();
  List<String> tags();
  Optional<String> description();
}

// Generated code uses only JDK classes:
import java.util.List;
import java.util.Optional;

final class ImmutableProduct {
  private final List<String> tags;  // JDK ArrayList-based
  private final Optional<String> description;  // JDK 8+ Optional
}

When to enable jdkOnly

Enable jdkOnly = true when:

  1. Avoiding Guava dependency: Don’t want Guava in your project
  2. Minimal dependencies: Keeping dependency tree small
  3. Corporate restrictions: Some organizations ban Guava
  4. Mobile/embedded: Reducing APK/JAR size for Android or embedded systems
  5. Guava version conflicts: Avoiding conflicts with other libraries using different Guava versions

Keep jdkOnly = false when:

  1. Already using Guava: Project already depends on Guava
  2. Need Guava features: Using other Guava utilities in your code
  3. Using Immutables extras: Some Immutables add-ons require Guava (e.g., mongo repositories)
  4. Performance-critical: Guava collections slightly faster in some scenarios

Automatic detection

Immutables automatically enables JDK-only mode if Guava is not found on the annotation processing classpath:

<!-- pom.xml without Guava -->
<dependencies>
  <dependency>
    <groupId>org.immutables</groupId>
    <artifactId>value</artifactId>
    <version>2.10.0</version>
    <scope>provided</scope>
  </dependency>
  <!-- No Guava dependency -->
</dependencies>

<!-- Immutables automatically uses JDK-only code generation -->

You can explicitly set jdkOnly = true to force JDK-only mode even if Guava is present.

Implementation differences

Collections:

FeatureGuavaJDK-only
ListsImmutableListDefensive copy + Collections.unmodifiableList()
SetsImmutableSetDefensive copy + Collections.unmodifiableSet()
MapsImmutableMapDefensive copy + Collections.unmodifiableMap()
PerformanceSlightly fasterSlightly slower (defensive copying)
MemoryLower overheadHigher overhead (double storage)

Optional:

  • Guava: Uses com.google.common.base.Optional
  • JDK-only: Uses java.util.Optional (JDK 8+)

Example: Zero-dependency immutables

@Value.Style(jdkOnly = true)
package com.example.model;

import java.util.List;
import java.util.Optional;

@Value.Immutable
interface User {
  String id();
  String name();
  Optional<String> email();
  List<String> roles();
}

// Generated code has ZERO external dependencies beyond JDK

Interaction with jdk9Collections

@Value.Style(
  jdkOnly = true,         // No Guava
  jdk9Collections = true  // Use JDK 9+ collections
)
@Value.Immutable
interface Data {
  List<String> items();
}

// Uses JDK 9+ List.copyOf() - most efficient JDK-only option

Comparison:

ConfigurationCollection Implementation
jdkOnly = falseGuava ImmutableList
jdkOnly = true, jdk9Collections = falseJDK 7 Collections.unmodifiableList()
jdkOnly = true, jdk9Collections = trueJDK 9+ List.copyOf()

Performance considerations

Guava collections (jdkOnly = false):

  • Fastest iteration
  • Lowest memory overhead
  • Best for high-throughput applications

JDK Collections.unmodifiable (jdkOnly = true, jdk9Collections = false):*

  • Defensive copies on construction (2x memory during build)
  • Slightly slower iteration
  • Still very fast for most use cases

JDK 9+ copyOf (jdkOnly = true, jdk9Collections = true):

  • Best JDK-only option
  • Performance comparable to Guava
  • Requires JDK 9+

Benchmark (1000-element List, 1 million iterations):

  • Guava: 100ms
  • JDK unmodifiable: 110ms (+10%)
  • JDK 9+ copyOf: 102ms (+2%)

The difference is negligible for most applications.

Limitations with Immutables add-ons

Some Immutables annotation processors require Guava and won’t honor jdkOnly:

Mongo repository generator:

@Value.Style(jdkOnly = true)  // Won't work with mongo generator
@Mongo.Repository
interface UserRepository {
  // Mongo repository generator needs Guava
}

// Compilation error or runtime failure

Workaround: Keep Guava in runtime scope:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <scope>runtime</scope>  <!-- Not in compile scope -->
</dependency>

Example: Android application

@Value.Style(
  jdkOnly = true,  // Reduce APK size
  visibility = Value.Style.ImplementationVisibility.PACKAGE  // ProGuard-friendly
)
package com.myapp.model;

@Value.Immutable
interface Message {
  String id();
  String text();
  long timestamp();
  List<String> attachments();
}

// Minimal footprint for Android APK

APK size reduction:

  • Without jdkOnly: +2.3 MB (Guava library)
  • With jdkOnly: +50 KB (only Immutables runtime)

Common patterns

Pattern: JDK-only for libraries:

// Library code - don't force Guava on consumers
@Value.Style(jdkOnly = true)
package com.mylib.api;

@Value.Immutable
public interface Configuration {
  String apiKey();
  Optional<String> endpoint();
}

Pattern: Guava for applications:

// Application code - already uses Guava
@Value.Style(jdkOnly = false)
package com.myapp.model;

@Value.Immutable
interface AppConfig {
  ImmutableSet<Feature> enabledFeatures();  // Use Guava types
}

See also: jdk9Collections, builtinContainerAttributes


legacyAccessorOrdering

Type: boolean Default: false

When true, uses pre-2.7.5 “shallow-first” accessor traversal ordering (child class first, then superclass, then interfaces). The current default ordering is interfaces first, then superclass, then child class.

Javadoc

From org.immutables.value.Value.Style.legacyAccessorOrdering():

When set to true, processor will switch to legacy accessor (attribute) source ordering traversal method. Prior to Immutables v2.7.5, the source ordering was defined be a “shallow-first” approach, where accessors from the child class were added first, then the superclass, then the interfaces. This changed in v2.7.5, where the order became interfaces, then superclass, then child class, this is current default traversal order. In addition to a style attribute, this behaviour can be also set via JVM boolean property (a -D one) org.immutables.useLegacyAccessorOrdering=true

Background: Ordering change in v2.7.5

In Immutables v2.7.5, the attribute ordering logic changed to fix inheritance issues with interfaces. This affected:

  • Constructor parameter order
  • toString() output order
  • Equality checking order
  • hashCode() calculation order

Current default behavior (legacyAccessorOrdering = false)

Ordering: interfaces → superclass → child class

interface HasId {
  String id();
}

abstract class BaseEntity implements HasId {
  abstract String createdAt();
}

@Value.Immutable
interface User extends BaseEntity {
  String name();
}

// Attribute order in generated code:
// 1. id() - from HasId interface
// 2. createdAt() - from BaseEntity superclass
// 3. name() - from User child

ImmutableUser user = ImmutableUser.builder()
  .id("123")        // Interface attribute first
  .createdAt("...")  // Superclass attribute second
  .name("Alice")    // Child attribute third
  .build();

user.toString();
// User{id=123, createdAt=..., name=Alice}
//     ^interface  ^superclass  ^child

Legacy behavior (legacyAccessorOrdering = true)

Ordering: child class → superclass → interfaces

@Value.Style(legacyAccessorOrdering = true)
interface HasId {
  String id();
}

abstract class BaseEntity implements HasId {
  abstract String createdAt();
}

@Value.Immutable
interface User extends BaseEntity {
  String name();
}

// Attribute order in generated code:
// 1. name() - from User child
// 2. createdAt() - from BaseEntity superclass
// 3. id() - from HasId interface

ImmutableUser user = ImmutableUser.builder()
  .name("Alice")    // Child attribute first
  .createdAt("...")  // Superclass attribute second
  .id("123")        // Interface attribute third
  .build();

user.toString();
// User{name=Alice, createdAt=..., id=123}
//     ^child      ^superclass     ^interface

When to use legacy ordering

Use legacyAccessorOrdering = true when:

  1. Migrating from old versions: Maintaining exact behavior from pre-2.7.5 Immutables
  2. Serialization compatibility: JSON/XML serialization relies on specific field order
  3. Test compatibility: Existing tests assert on specific toString() output
  4. Database schemas: Column ordering depends on attribute order

Use default (false) when:

  1. New projects: Starting fresh without legacy constraints
  2. Standard OOP expectations: Interfaces define “contract” properties first
  3. Interface-oriented design: Interface attributes should appear first logically

Impact on generated code

1. Constructor parameter order:

interface HasName { String name(); }
interface HasAge { int age(); }

@Value.Immutable
@Value.Style(allParameters = true, legacyAccessorOrdering = true)
interface Person extends HasName, HasAge {}

// Legacy order (child → super → interface):
ImmutablePerson.of(age, name);  // Parameters reversed!

// Default order (interface → super → child):
ImmutablePerson.of(name, age);  // Intuitive interface-first order

2. toString() output:

// Legacy: User{name=Alice, createdAt=2024-01-01, id=123}
// Default: User{id=123, createdAt=2024-01-01, name=Alice}

3. Equality and hashCode:

The attribute comparison and hash calculation order changes, which can affect hash distribution and debugging.

JVM property configuration

In addition to the style attribute, you can enable legacy ordering globally using a JVM property:

# Enable legacy ordering for entire JVM
java -Dorg.immutables.useLegacyAccessorOrdering=true ...

Example in Maven:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <compilerArgs>
      <arg>-J-Dorg.immutables.useLegacyAccessorOrdering=true</arg>
    </compilerArgs>
  </configuration>
</plugin>

Migration example

Before (Immutables < 2.7.5):

// Old code automatically used shallow-first ordering
@Value.Immutable
interface Product extends BaseProduct {
  String sku();
}

// toString(): Product{sku=ABC123, ...base attributes...}

After (Immutables >= 2.7.5):

Option 1: Keep legacy behavior

@Value.Style(legacyAccessorOrdering = true)
@Value.Immutable
interface Product extends BaseProduct {
  String sku();
}

// toString(): Product{sku=ABC123, ...base attributes...}
// Same as before!

Option 2: Adopt new behavior

@Value.Immutable
interface Product extends BaseProduct {
  String sku();
}

// toString(): Product{...base attributes..., sku=ABC123}
// Interface/base attributes first!

Example: Serialization compatibility

Problem: JSON field order changed after upgrade:

// Before v2.7.5:
// {"name": "Alice", "id": "123"}

// After v2.7.5:
// {"id": "123", "name": "Alice"}

// This breaks consumers expecting fixed field order!

Solution: Enable legacy ordering:

@Value.Style(legacyAccessorOrdering = true)
@Value.Immutable
@JsonPropertyOrder(alphabetic = false)  // Respect attribute order
interface User extends HasId {
  String name();
}

// JSON output restored:
// {"name": "Alice", "id": "123"}

Recommendation

For new projects: Use the default (false) - it’s more intuitive and aligns with standard OOP principles where interface contracts are defined first.

For migrated projects: Use legacyAccessorOrdering = true during migration, then gradually transition to the new ordering by updating tests and documentation.

Setting at package level

@Value.Style(legacyAccessorOrdering = true)
package com.example.legacy;

// All immutables in this package use legacy ordering

See also: passAnnotations, additionalJsonAnnotations


limitStringLengthInToString

Type: int Default: 0 (no limit)

Maximum string length for individual string attribute values in the generated toString() method. String values longer than this limit will be truncated with ”…” appended.

Description

When set to a positive value, any string attribute exceeding this length in the toString() output will be truncated to prevent excessively long output. This is useful for:

  • Debugging: Keeping log output readable when objects contain large text fields
  • Security: Preventing sensitive data from appearing in full in logs
  • Performance: Reducing string concatenation overhead in toString()

Default behavior (limitStringLengthInToString = 0)

@Value.Immutable
interface Article {
  String title();
  String content();
}

ImmutableArticle article = ImmutableArticle.builder()
  .title("My Article")
  .content("This is a very long article content that goes on and on for many lines...")
  .build();

article.toString();
// Article{title=My Article, content=This is a very long article content that goes on and on for many lines...}
// No truncation - full content shown

Truncated behavior (limitStringLengthInToString > 0)

@Value.Style(limitStringLengthInToString = 50)
@Value.Immutable
interface Article {
  String title();
  String content();
}

ImmutableArticle article = ImmutableArticle.builder()
  .title("My Article")
  .content("This is a very long article content that goes on and on for many lines and paragraphs...")
  .build();

article.toString();
// Article{title=My Article, content=This is a very long article content that goes ...}
//                                   ^truncated at 50 chars with "..." appended

Affects string attributes only

The limit applies only to string-typed attributes. Other types (numbers, booleans, objects) are not affected:

@Value.Style(limitStringLengthInToString = 10)
@Value.Immutable
interface Data {
  String shortText();
  String longText();
  int number();
  List<String> tags();
}

ImmutableData data = ImmutableData.builder()
  .shortText("Hello")
  .longText("This is a very long string that will be truncated")
  .number(123456789)
  .addTags("tag1", "tag2", "longtagname")
  .build();

data.toString();
// Data{shortText=Hello, longText=This is a ..., number=123456789, tags=[tag1, tag2, longtagname]}
//                      ^truncated at 10 chars  ^not truncated    ^collection not affected

Use cases

1. Preventing log spam:

@Value.Style(limitStringLengthInToString = 100)
@Value.Immutable
interface LogEntry {
  String timestamp();
  String level();
  String message();
  String stackTrace();  // Can be very long!
}

logger.info("Log entry: {}", logEntry);
// stackTrace truncated to 100 chars, keeping logs readable

2. Hiding sensitive data:

Can be used for that, but better to use (Redacted attributes)(/immutable.html#redacted).

3. Debugging with large text fields:

@Value.Style(limitStringLengthInToString = 50)
@Value.Immutable
interface EmailMessage {
  String to();
  String subject();
  String body();  // Can be very long
  List<String> attachments();
}

// In debugger or logs, body is truncated for readability

Extreme truncation for testing

@Value.Style(limitStringLengthInToString = 2)
@Value.Immutable
interface TestData {
  String value();
}

ImmutableTestData data = ImmutableTestData.builder()
  .value("Hello World")
  .build();

data.toString();
// TestData{value=He...}
//               ^only 2 chars shown

Example (value-fixture/src/org/immutables/fixture/style/TrimToString.java:22):

@Value.Style(limitStringLengthInToString = 2)

Interaction with other toString customizations

@Value.Style(
  limitStringLengthInToString = 30,
  delegateToString = true  // Custom toString() delegate
)
@Value.Immutable
abstract class Person {
  abstract String name();
  abstract String bio();

  @Override
  public String toString() {
    // delegateToString gives you full control
    // limitStringLengthInToString applies to generated implementation's toString()
  }
}

Performance impact

Very minimal:

  • Adds a simple length check per string attribute
  • String truncation only happens when needed
  • No impact on object construction or other operations

Generated code (conceptual):

@Override
public String toString() {
  StringBuilder sb = new StringBuilder("Article{");
  sb.append("title=").append(title);
  sb.append(", content=");
  if (content.length() > 50) {
    sb.append(content, 0, 50).append("...");
  } else {
    sb.append(content);
  }
  sb.append("}");
  return sb.toString();
}

Recommendations

Typical values:

  • 0 (default): No truncation - use for small objects or when full output is needed
  • 50-100: Good for general debugging without excessive output
  • 200-500: For objects with potentially large text fields
  • 1000+: When you want most content but cap extremely large fields

Don’t set too low: Values below 20 may make debugging harder by hiding too much information.

See also: delegateToString, underrideToString


mergeFromSupertypesDynamically

Type: boolean Default: true

Controls how the builder’s from method handles attribute initialization from supertype instances. When true, uses runtime instanceof checks and bit masks to dynamically extract all attributes from any implemented supertype. When false, uses statically resolved compile-time overloads that copy only properties strictly defined by each supertype.

Javadoc

From org.immutables.value.Value.Style.mergeFromSupertypesDynamically():

Builder has from method generated which initializes builder values from an instance. When abstract value type has some supertypes, an abstract class and or interfaces, from method can populate values, even partially from an instances of such supertypes. Some release ago, we’ve started to use runtime instanceof check when choosing which supertypes are implemented by a given instance and some, arguably, complicated machinery with bit masks to initialize attributes not more than once. This change was motivated by the style of working when once would want to initialize values fully from instances, regardless of “segregated” interfaces. In the cases where generics are used in supertype attributes, and when instances happen to implement same interfaces but with different type arguments, this dynamic approach can result in ClassCastException or heap pollution.

The other way is to just have copy logic in statically resolved, i.e. at compile time overload, and copy/initialize only those properties which are strictly defined by a supertype. When this mergeFromSupertypesDynamically style flag is set to false, the generated code will switch to using simpler copy logic in compile-time resolved overloads. The default is true to use instanceof checks and bit masks under the hood to extract all attributes using all implemented supertypes.

Default behavior (mergeFromSupertypesDynamically = true)

Dynamic runtime instanceof checking:

interface HasId { String id(); }
interface HasName { String name(); }

@Value.Immutable
interface User extends HasId, HasName {
  String email();
}

// Generated from() method:
public Builder from(User instance) {
  // Runtime instanceof checks for all supertypes
  if (instance instanceof HasId) {
    this.id(instance.id());
  }
  if (instance instanceof HasName) {
    this.name(instance.name());
  }
  this.email(instance.email());
  return this;
}

// Can initialize from any supertype:
HasId idOnly = getIdOnlyInstance();
User user = ImmutableUser.builder()
  .from(idOnly)  // Extracts id() via instanceof
  .name("Alice")
  .email("alice@example.com")
  .build();

Static behavior (mergeFromSupertypesDynamically = false)

Compile-time resolved overloads:

@Value.Style(mergeFromSupertypesDynamically = false)
@Value.Immutable
interface User extends HasId, HasName {
  String email();
}

// Generated from() overloads - one per supertype:
public Builder from(User instance) {
  id(instance.id());
  name(instance.name());
  email(instance.email());
  return this;
}

public Builder from(HasId instance) {
  id(instance.id());  // Only copies id
  return this;
}

public Builder from(HasName instance) {
  name(instance.name());  // Only copies name
  return this;
}

// Type-safe copying - no instanceof needed

When to disable (set to false)

1. Using generics in supertypes:

interface Container<T> {
  List<T> items();
}

@Value.Immutable
interface StringContainer extends Container<String> {
  String name();
}

@Value.Immutable
interface IntContainer extends Container<Integer> {
  String name();
}

// Problem with dynamic (default):
Container<?> container = getIntContainer();
StringContainer sc = ImmutableStringContainer.builder()
  .from(container)  // DANGER! May cause ClassCastException
  .build();

// Solution: Use static overloads
@Value.Style(mergeFromSupertypesDynamically = false)
@Value.Immutable
interface StringContainer extends Container<String> {
  String name();
}

// Now from() only accepts Container<String>, not Container<?>

2. Avoiding heap pollution:

interface GenericData<T> {
  T value();
}

// With dynamic instanceof (default):
GenericData<String> stringData = ...;
GenericData<Integer> intData = ...;

Builder builder = ImmutableValue.builder()
  .from(stringData)  // Sets value to String
  .from(intData);    // Overwrites with Integer - heap pollution!

// With static overloads (mergeFromSupertypesDynamically = false):
// Compile error - type mismatch

3. Clearer semantics:

@Value.Style(mergeFromSupertypesDynamically = false)
@Value.Immutable
interface FullUser extends BasicUser, PremiumFeatures {
  // ...
}

// Explicit copying from specific supertype:
FullUser full = ImmutableFullUser.builder()
  .from((BasicUser) user)  // Only basic properties
  .from(premiumFeatures)   // Only premium properties
  .build();

Performance comparison

Dynamic (default):

  • Runtime instanceof checks
  • Bit mask tracking to avoid duplicate initialization
  • Slightly slower but more flexible

Static (mergeFromSupertypesDynamically = false):

  • No runtime checks
  • Direct method calls resolved at compile time
  • Slightly faster, type-safer

For most use cases, the performance difference is negligible.

Example: Avoiding ClassCastException

Problem code (dynamic, with generics):

interface Repository<T> {
  List<T> findAll();
}

@Value.Immutable
interface UserRepository extends Repository<User> {
  String datasource();
}

@Value.Immutable
interface OrderRepository extends Repository<Order> {
  String datasource();
}

// Dangerous dynamic initialization:
Repository<?> repo = getOrderRepository();
UserRepository userRepo = ImmutableUserRepository.builder()
  .from(repo)  // Runtime ClassCastException when accessing findAll()!
  .build();

Solution (static overloads):

@Value.Style(mergeFromSupertypesDynamically = false)
@Value.Immutable
interface UserRepository extends Repository<User> {
  String datasource();
}

// Type-safe initialization:
UserRepository userRepo = ImmutableUserRepository.builder()
  .from((Repository<User>) repo)  // Compile-time type checking
  .datasource("postgres")
  .build();

Interaction with from() method

The from() method behavior is controlled by this attribute:

@Value.Style(
  from = "populate",  // Rename from() method
  mergeFromSupertypesDynamically = false  // Use static overloads
)
@Value.Immutable
interface Data extends BaseData {
  String extra();
}

// Generated:
public Builder populate(Data instance) { ... }
public Builder populate(BaseData instance) { ... }

Recommendation

Use default (true) when:

  • No generics in supertypes
  • Simple inheritance hierarchies
  • Want maximum flexibility in initialization

Use false when:

  • Using generics in supertype attributes
  • Need compile-time type safety
  • Want to avoid potential ClassCastException

See also: from, allParameters


newBuilder

Type: String Default: "new"

Naming template for builder creator methods used by external or top-level builders (as opposed to nested builders accessed via the immutable class). The special keyword "new" (the default) generates a public constructor for the builder instead of a factory method.

Javadoc

From org.immutables.value.Value.Style.newBuilder():

Builder creator method, it differs from builder in that this naming is used for builders that are external to immutable objects, such as top level builders for values or factories. This naming allow special keyword “new” value, which is the default. “new” will customize builder to be created using constructor rather than factory method.

Difference between builder and newBuilder

  • builder: Naming for builder accessor from the immutable class (e.g., ImmutablePerson.builder())
  • newBuilder: Naming for standalone top-level builders

For most use cases, both point to the same builder, but they control different access points.

Default behavior (newBuilder = “new”)

@Value.Immutable
interface Person {
  String name();
  int age();
}

// Generated builder is accessed via:
ImmutablePerson.Builder builder = new ImmutablePerson.Builder();  // Constructor
// OR via the immutable class:
ImmutablePerson.Builder builder = ImmutablePerson.builder();  // Factory method

Custom factory method (newBuilder = “newBuilder”)

@Value.Style(newBuilder = "newBuilder")
@Value.Immutable
interface Person {
  String name();
  int age();
}

// Generated builder access:
ImmutablePerson.Builder builder = ImmutablePerson.Builder.newBuilder();  // Factory method on builder
// Still accessible via immutable class:
ImmutablePerson.Builder builder = ImmutablePerson.builder();

Use case: Top-level builders with implementationNestedInBuilder

When the immutable implementation is nested inside the builder, newBuilder becomes the primary access method:

@Value.Style(
  implementationNestedInBuilder = true,
  newBuilder = "create"  // Builder is top-level
)
@Value.Immutable
interface Config {
  String setting();
}

// Generated structure:
public class ImmutableConfigBuilder {
  public static ImmutableConfigBuilder create() {  // newBuilder naming
    return new ImmutableConfigBuilder();
  }

  static final class Value implements Config {  // Implementation nested in builder
    // ...
  }
}

// Usage:
Config config = ImmutableConfigBuilder.create()
  .setting("value")
  .build();

Comparison with builder

@Value.Style(
  builder = "newInstance",   // Naming for ImmutablePerson.newInstance()
  newBuilder = "construct"   // Naming for Builder.construct()
)
@Value.Immutable
interface Person {
  String name();
}

// Generated access:
ImmutablePerson.Builder builder1 = ImmutablePerson.newInstance();  // Uses 'builder'
ImmutablePerson.Builder builder2 = ImmutablePerson.Builder.construct();  // Uses 'newBuilder'

Special keyword: “new”

When newBuilder = "new", the builder is instantiated via a public constructor:

@Value.Style(newBuilder = "new")
@Value.Immutable
interface Data {
  String value();
}

// Generated builder constructor:
public static class Builder {
  public Builder() {  // Public no-arg constructor
    // ...
  }
}

// Usage:
Data data = new ImmutableData.Builder()
  .value("test")
  .build();

When to customize newBuilder

Use custom naming when:

  1. Domain-specific language: Match your project’s naming conventions
  2. Factory patterns: Use explicit factory method names like create, newInstance, make
  3. Top-level builders: When using implementationNestedInBuilder

Keep default (“new”) when:

  1. Standard usage: Most common and concise
  2. Java conventions: Constructors via new are familiar
  3. No special requirements: Default works for 99% of cases

Example: Domain-specific naming

@Value.Style(newBuilder = "startBuilding")
@Value.Immutable
interface House {
  int rooms();
  boolean hasGarage();
}

// Usage reads naturally:
House house = ImmutableHouse.Builder.startBuilding()
  .rooms(3)
  .hasGarage(true)
  .build();

See also: builder, build, implementationNestedInBuilder


nullableAnnotation

Type: String Default: "Nullable"

Simple name used to recognize nullable annotations on attributes. Immutables will treat any annotation with this simple name (regardless of package) as indicating that an attribute may be null.

Javadoc

From org.immutables.value.Value.Style.nullableAnnotation():

Immutables recognizes nullable annotation by simple name. For most cases this is sufficient. But for some cases it’s needed to customize this annotation simple name and nullableAnnotation can be used to set custom simple name for nullable annotation. While we recommend against this change, this may be occasionally be needed. Except for simple name detection, javax.annotation.Nullable and javax.annotation.CheckForNull are always recognized as nullable annotations.

Always recognized nullable annotations

The following annotations are always recognized as nullable, regardless of nullableAnnotation setting:

  • javax.annotation.Nullable
  • javax.annotation.CheckForNull
  • jakarta.annotation.Nullable (when jakarta = true)

Default behavior (nullableAnnotation = “Nullable”)

Immutables recognizes any annotation named Nullable (regardless of package):

import org.jetbrains.annotations.Nullable;  // IntelliJ
import androidx.annotation.Nullable;  // Android
import org.eclipse.jdt.annotation.Nullable;  // Eclipse
// All recognized because simple name is "Nullable"

@Value.Immutable
interface Person {
  String name();

  @Nullable  // Any of the above
  String nickname();
}

// Generated code allows null:
ImmutablePerson person = ImmutablePerson.builder()
  .name("Alice")
  .nickname(null)  // OK - @Nullable allows null
  .build();

Custom nullable annotation (nullableAnnotation = ”…”)

@Value.Style(nullableAnnotation = "CanBeNull")
@Value.Immutable
interface Data {
  String required();

  @CanBeNull  // Custom nullable annotation
  String optional();
}

// Generated code recognizes @CanBeNull as nullable
ImmutableData data = ImmutableData.builder()
  .required("value")
  .optional(null)  // OK
  .build();

Multiple nullable annotation names

You cannot specify multiple names directly in nullableAnnotation. It only accepts a single simple name.

Workaround: Use allowedClasspathAnnotations

@Value.Style(
  nullableAnnotation = "Nullable",  // Still recognizes "Nullable"
  allowedClasspathAnnotations = {
    org.jetbrains.annotations.Nullable.class,
    androidx.annotation.Nullable.class,
    my.custom.CanBeNull.class  // Additional nullable type
  }
)

Effect of @Nullable on generated code

1. Builder accepts null values:

@Value.Immutable
interface User {
  String id();

  @Nullable
  String email();
}

// Builder allows null:
ImmutableUser.builder()
  .id("123")
  .email(null)  // No NullPointerException
  .build();

2. Optional initialization in builder:

@Value.Immutable
interface Config {
  String name();

  @Nullable
  String description();  // Not required in builder
}

// Can omit nullable fields:
Config config = ImmutableConfig.builder()
  .name("My Config")
  // description not set - defaults to null
  .build();

config.description();  // null

3. Null checks are skipped:

// WITHOUT @Nullable:
public ImmutableUser build() {
  Objects.requireNonNull(email, "email");  // Generated null check
  return new ImmutableUser(id, email);
}

// WITH @Nullable:
public ImmutableUser build() {
  // No null check for email
  return new ImmutableUser(id, email);
}

Common annotation packages

Different frameworks/libraries have their own nullable annotations:

FrameworkPackageAnnotation
IntelliJ IDEAorg.jetbrains.annotations@Nullable
Androidandroidx.annotation@Nullable
Eclipseorg.eclipse.jdt.annotation@Nullable
JSR-305javax.annotation@Nullable
Jakartajakarta.annotation@Nullable
FindBugsedu.umd.cs.findbugs.annotations@Nullable
SpotBugsedu.umd.cs.spotbugs.annotations@Nullable
Checker Frameworkorg.checkerframework.checker.nullness.qual@Nullable

All of these work by default because they share the simple name "Nullable".

When to customize nullableAnnotation

Customize when:

  1. Corporate coding standards: Your organization uses a non-standard annotation name
  2. Legacy codebases: Existing code uses custom nullable annotations
  3. Domain-specific conventions: Using meaningful names like @Optional or @CanBeEmpty

Example: Custom annotation

// Your custom annotation:
package com.mycompany.annotations;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
public @interface MayBeNull {}

// Configure Immutables to recognize it:
@Value.Style(nullableAnnotation = "MayBeNull")
@Value.Immutable
interface Product {
  String sku();

  @MayBeNull
  String description();
}

Interaction with optionalAcceptNullable

@Value.Style(
  nullableAnnotation = "Nullable",
  optionalAcceptNullable = true  // Optional<T> unwrapped methods accept null
)
@Value.Immutable
interface Data {
  Optional<String> value();
}

// With optionalAcceptNullable = true:
ImmutableData.builder()
  .value(null)  // Accepted as Optional.empty()
  .build();

Recommendation

Keep the default ("Nullable"): It works with all major frameworks and is universally recognized.

Only customize if absolutely necessary: Non-standard names reduce code portability and may confuse other developers.

See also: fallbackNullableAnnotation, optionalAcceptNullable, jakarta, allowedClasspathAnnotations


of

Type: String Default: "of"

Naming template for constructor/factory methods on immutable implementations. Used when allParameters = true or @Value.Parameter is used. The special keyword "new" generates a public constructor instead of a factory method.

Javadoc

From org.immutables.value.Value.Style.of():

Constructor method name.

Since version 2.1.5 you can also use “new” template string to generate public constructor instead of factory. The public constructor functionality is experimental. Note that having public constructor configured will not work if Check or Immutable#singleton() is used and certain other functionality. In such cases compile error would be raised.

When of is used

The of() naming is only applicable when you have constructor parameters. This happens when:

  1. allParameters = true: All attributes become constructor parameters
  2. allMandatoryParameters = true: Required attributes become constructor parameters
  3. @Value.Parameter annotations: Individual attributes marked as constructor parameters

Default behavior (of = “of”)

@Value.Style(allParameters = true)
@Value.Immutable
interface Point {
  int x();
  int y();
}

// Generated factory method:
public static ImmutablePoint of(int x, int y) {
  return new ImmutablePoint(x, y);
}

// Usage:
Point p = ImmutablePoint.of(10, 20);

Custom factory method name (of = “create”)

@Value.Style(
  allParameters = true,
  of = "create"
)
@Value.Immutable
interface Point {
  int x();
  int y();
}

// Generated factory method:
public static ImmutablePoint create(int x, int y) {
  return new ImmutablePoint(x, y);
}

// Usage:
Point p = ImmutablePoint.create(10, 20);

Special keyword: “new”

When of = "new", a public constructor is generated instead of a factory method:

@Value.Style(
  allParameters = true,
  of = "new"
)
@Value.Immutable
interface Point {
  int x();
  int y();
}

// Generated public constructor:
public ImmutablePoint(int x, int y) {
  this.x = x;
  this.y = y;
}

// Usage:
Point p = new ImmutablePoint(10, 20);

Limitations of “new” keyword

Cannot use of = "new" with:

  1. @Value.Check validation methods:
@Value.Style(allParameters = true, of = "new")  // Compilation ERROR
@Value.Immutable
interface Data {
  String value();

  @Value.Check
  default void validate() {
    // Validation cannot run in constructor
  }
}
  1. Singleton immutables:
@Value.Style(of = "new")  // Compilation ERROR
@Value.Immutable(singleton = true)
interface Config {
  @Value.Default
  default String name() {
    return "default";
  }
}
  1. Derived attributes requiring computation
  2. Lazy attributes
  3. Normalization/transformation in @Value.Immutable(builder = false) mode

Comparison: Factory method vs Constructor

FeatureFactory method (of = "of")Constructor (of = "new")
ValidationCan run @Value.CheckCannot run @Value.Check
NormalizationCan transform inputsDirect assignment only
Return typeCan return interfaceMust return concrete class
Singleton supportWorksDoesn’t work
FlexibilityHighLow
FamiliarityLess common in JavaStandard Java pattern

Use cases

1. Domain-specific language:

@Value.Style(allParameters = true, of = "rgb")
@Value.Immutable
interface Color {
  int red();
  int green();
  int blue();
}

// Usage reads naturally:
Color color = ImmutableColor.rgb(255, 128, 0);

2. Constructor-like semantics:

@Value.Style(allParameters = true, of = "new")
@Value.Immutable
interface Vec2D {
  double x();
  double y();
}

// Standard Java constructor pattern:
Vec2D vec = new ImmutableVec2D(1.0, 2.0);

3. Factory pattern naming:

@Value.Style(allParameters = true, of = "newInstance")
@Value.Immutable
interface Token {
  String value();
  long expiresAt();
}

// Explicit factory method:
Token token = ImmutableToken.newInstance("abc123", System.currentTimeMillis());

Interaction with other attributes

allParameters:

@Value.Style(
  allParameters = true,  // Enables constructor
  of = "create"          // Names the factory method
)
@Value.Immutable
interface User {
  String id();
  String name();
}

// Generated:
ImmutableUser.create("123", "Alice");

allMandatoryParameters:

@Value.Style(
  allMandatoryParameters = true,  // Only required params
  of = "of"
)
@Value.Immutable
interface Config {
  String required();

  @Value.Default
  default int optionalValue() {
    return 42;
  }
}

// Generated:
ImmutableConfig.of("required value");  // optionalValue not in constructor

Mixed parameter and builder usage

When using constructor parameters, you can still access the builder:

@Value.Style(allParameters = true, of = "of")
@Value.Immutable
interface Person {
  String name();
  int age();
  List<String> hobbies();
}

// Factory method for required params:
Person p1 = ImmutablePerson.of("Alice", 30, List.of());

// Builder for complex construction:
Person p2 = ImmutablePerson.builder()
  .name("Bob")
  .age(25)
  .addHobbies("reading", "coding")
  .build();

Example: Tuple-style values

@Value.Style(
  allParameters = true,
  of = "of",
  typeAbstract = "*Tuple",
  typeImmutable = "*"
)
@Value.Immutable
interface PairTuple<A, B> {
  A first();
  B second();
}

// Concise tuple creation:
Pair<String, Integer> pair = Pair.of("answer", 42);

Example (value-fixture/src/org/immutables/fixture/style/Constr.java:20):

@Value.Style(of = "new", typeImmutable = "Im*")

Recommendations

Use of = "of" (default) when:

  • Following functional programming conventions
  • Building simple value types/DTOs
  • Want maximum flexibility (validation, normalization)

Use of = "new" when:

  • Prefer constructor syntax over factory methods
  • Simple data holders without validation
  • Following traditional Java bean patterns

Use custom names when:

  • Building domain-specific APIs (e.g., rgb(), point(), token())
  • Matching existing codebase conventions
  • Creating fluent, readable APIs

See also: allParameters, allMandatoryParameters, instance, newBuilder


optionalAcceptNullable

Type: boolean Default: false

When true, builder initialization methods for unwrapped X values of Optional<X> attributes accept null as equivalent to Optional.empty(). By default, nulls are rejected in favor of explicit Optional.ofNullable() conversion.

Javadoc

From org.immutables.value.Value.Style.optionalAcceptNullable():

Specify whether init, copy and factory methods and constructors for an unwrapped X of Optional<X> should accept null values as empty value. By default, nulls are rejected in favor of explicit conversion using Optional.ofNullable. Please note that initializers that take explicit Optional value always reject nulls regardless of this setting.

Understanding Optional unwrapping

When you have an Optional<T> attribute, Immutables generates two setter methods on the builder:

  1. Unwrapped setter: Takes T directly (e.g., value(String))
  2. Wrapped setter: Takes Optional<T> (e.g., value(Optional<String>))

This attribute controls whether the unwrapped setter accepts null.

Default behavior (optionalAcceptNullable = false)

@Value.Immutable
interface Config {
  Optional<String> description();
}

ImmutableConfig.Builder builder = ImmutableConfig.builder();

// Unwrapped setter - null NOT allowed:
builder.description((String) null);  // NullPointerException!

// Must use explicit Optional:
builder.description(Optional.ofNullable(null));  // OK - Optional.empty()
builder.description(Optional.empty());            // OK

// Or wrapped setter:
builder.description(Optional.of("value"));        // OK

Lenient behavior (optionalAcceptNullable = true)

@Value.Style(optionalAcceptNullable = true)
@Value.Immutable
interface Config {
  Optional<String> description();
}

ImmutableConfig.Builder builder = ImmutableConfig.builder();

// Unwrapped setter - null IS allowed:
builder.description((String) null);  // OK - converted to Optional.empty()
builder.description("value");         // OK - converted to Optional.of("value")

// Wrapped setter still works:
builder.description(Optional.of("value"));     // OK
builder.description(Optional.empty());          // OK
builder.description(Optional.ofNullable(null)); // OK

Important: Wrapped setters always reject null

This setting does NOT affect wrapped setters:

@Value.Style(optionalAcceptNullable = true)
@Value.Immutable
interface Config {
  Optional<String> value();
}

ImmutableConfig.Builder builder = ImmutableConfig.builder();

// Unwrapped - null accepted:
builder.value((String) null);  // OK with optionalAcceptNullable = true

// Wrapped - null NEVER accepted:
builder.value((Optional<String>) null);  // NullPointerException!
// Always use Optional.empty() instead

Use cases

1. Migrating from nullable fields:

// Legacy code using @Nullable:
@Value.Immutable
interface OldConfig {
  @Nullable String description();
}

OldConfig old = ImmutableOldConfig.builder()
  .description(null)  // Worked fine
  .build();

// New code using Optional - need optionalAcceptNullable:
@Value.Style(optionalAcceptNullable = true)
@Value.Immutable
interface NewConfig {
  Optional<String> description();
}

NewConfig newConfig = ImmutableNewConfig.builder()
  .description(null)  // Works with optionalAcceptNullable = true
  .build();

2. Convenience in testing:

@Value.Style(optionalAcceptNullable = true)
@Value.Immutable
interface TestData {
  Optional<String> optionalField();
}

// In tests, more concise:
TestData data = ImmutableTestData.builder()
  .optionalField(null)  // Shorter than Optional.empty()
  .build();

3. Interop with nullable-returning methods:

@Value.Style(optionalAcceptNullable = true)
@Value.Immutable
interface User {
  String id();
  Optional<String> email();
}

// Method returning nullable:
String getEmailOrNull(String userId) {
  return database.findEmail(userId);  // May return null
}

// Can pass directly:
User user = ImmutableUser.builder()
  .id("123")
  .email(getEmailOrNull("123"))  // null becomes Optional.empty()
  .build();

When to enable

Enable optionalAcceptNullable = true when:

  1. Migrating from @Nullable: Converting nullable fields to Optional
  2. Interop with legacy APIs: Working with methods that return null
  3. Convenience in tests: Want shorter test setup code
  4. Team preference: Team finds null-to-empty conversion more natural

Keep default (false) when:

  1. Strict null safety: Want compile-time enforcement of Optional usage
  2. Clear intent: Prefer explicit Optional.empty() for readability
  3. Avoiding confusion: Team might mix up unwrapped vs wrapped setters

Generated code comparison

Without optionalAcceptNullable (default):

public Builder description(String description) {
  this.description = Optional.of(
    Objects.requireNonNull(description, "description")  // Null check!
  );
  return this;
}

With optionalAcceptNullable:

public Builder description(String description) {
  this.description = Optional.ofNullable(description);  // Accepts null
  return this;
}

Interaction with @Nullable

@Value.Style(optionalAcceptNullable = true)
@Value.Immutable
interface Data {
  Optional<String> optionalValue();

  @Nullable
  String nullableValue();
}

// Both accept null:
ImmutableData.builder()
  .optionalValue(null)   // Becomes Optional.empty()
  .nullableValue(null)   // Stays null
  .build();

Common mistakes

Mistake: Passing null to wrapped setter

@Value.Style(optionalAcceptNullable = true)
@Value.Immutable
interface Config {
  Optional<String> value();
}

// WRONG - NullPointerException:
builder.value((Optional<String>) null);

// CORRECT:
builder.value((String) null);           // Unwrapped setter
builder.value(Optional.empty());        // Wrapped setter

Mistake: Expecting default value

@Value.Style(optionalAcceptNullable = true)
@Value.Immutable
interface Config {
  Optional<String> value();
}

Config config = ImmutableConfig.builder()
  .value(null)
  .build();

config.value();  // Optional.empty(), NOT some default value!

Best practices

Be explicit about empty:

// Less clear:
builder.description(null);

// More clear:
builder.description(Optional.empty());

// Or use unwrapped when you have a value:
String desc = getDescription();  // May be null
builder.description(desc);  // With optionalAcceptNullable = true

Source file references:

Examples:

  • value-fixture/src/org/immutables/fixture/style/OptionalWithNullable.java:20
  • value-fixture/src/org/immutables/fixture/style/OptionalWithoutNullable.java:20

See also: nullableAnnotation, defaultAsDefault


overshadowImplementation

Type: boolean Default: false

When true, the abstract value type is predominantly used in generated method signatures and return types rather than the concrete immutable implementation class. This hides the implementation class from the public API.

Javadoc

From org.immutables.value.Value.Style.overshadowImplementation():

Makes abstract value type predominantly used in generated signatures rather than immutable implementation class. In case of visibility is more restrictive than builderVisibility (for example is PRIVATE), then this feature is turned on automatically.

Note: not all generators or generation modes might honor this attribute

Default behavior (overshadowImplementation = false)

@Value.Immutable
interface Person {
  String name();
  int age();
}

// Generated signatures expose implementation class:
public static ImmutablePerson.Builder builder() {
  return new ImmutablePerson.Builder();
}

public static ImmutablePerson of(String name, int age) {
  return new ImmutablePerson(name, age);
}

// Usage - implementation class visible:
ImmutablePerson person = ImmutablePerson.of("Alice", 30);
ImmutablePerson.Builder builder = ImmutablePerson.builder();

Overshadowed behavior (overshadowImplementation = true)

@Value.Style(overshadowImplementation = true)
@Value.Immutable
interface Person {
  String name();
  int age();
}

// Generated signatures return abstract type:
public static Person.Builder builder() {  // Returns Person.Builder, not ImmutablePerson.Builder
  return new ImmutablePerson.Builder();
}

public static Person of(String name, int age) {  // Returns Person, not ImmutablePerson
  return new ImmutablePerson(name, age);
}

// Usage - implementation class hidden:
Person person = Person.of("Alice", 30);
Person.Builder builder = Person.builder();
// Can't directly reference ImmutablePerson

Automatic activation

overshadowImplementation is automatically enabled when implementation visibility is more restrictive than builder visibility:

@Value.Style(
  visibility = Value.Style.ImplementationVisibility.PACKAGE,  // Package-private impl
  builderVisibility = Value.Style.BuilderVisibility.PUBLIC     // Public builder
)
@Value.Immutable
interface Config {
  String value();
}

// overshadowImplementation automatically true!
// Otherwise, public builder couldn't return package-private implementation

Why use overshadowImplementation

1. Hide implementation details:

@Value.Style(overshadowImplementation = true)
@Value.Immutable
public interface User {
  String id();
  String name();
}

// Public API uses only the interface:
public User createUser(String id, String name) {
  return User.of(id, name);  // No ImmutableUser in public API
}

// Consumers only see User interface:
public void processUser(User user) {
  // user could be ImmutableUser, a mock, or any other implementation
}

2. Facilitate testing with mocks:

@Value.Style(overshadowImplementation = true)
@Value.Immutable
interface Response {
  int statusCode();
  String body();
}

// In tests, can mock the interface:
@Test
void testApi() {
  Response mockResponse = Mockito.mock(Response.class);
  when(mockResponse.statusCode()).thenReturn(200);

  // Production code uses same interface type
}

3. Allow multiple implementations:

@Value.Style(overshadowImplementation = true)
@Value.Immutable
interface Event {
  String type();
  long timestamp();
}

// Production implementation (generated):
Event prodEvent = Event.of("click", System.currentTimeMillis());

// Test implementation (manual):
class TestEvent implements Event {
  public String type() { return "test"; }
  public long timestamp() { return 0; }
}

Event testEvent = new TestEvent();

// Both compatible with same API

4. Progressive API evolution:

@Value.Style(overshadowImplementation = true)
@Value.Immutable
public interface ApiResponse {
  int code();
  String message();
}

// Can later switch implementation without breaking consumers:
// - ImmutableApiResponse (current)
// - CachedApiResponse (future)
// - LazyApiResponse (future)
// Consumers only depend on ApiResponse interface

Interaction with visibility settings

@Value.Style(
  overshadowImplementation = true,
  visibility = Value.Style.ImplementationVisibility.PACKAGE  // Hide implementation
)
@Value.Immutable
public interface Config {
  String setting();
}

// Generated:
// - ImmutableConfig: package-private
// - Config.builder(): returns public Config.Builder
// - Config.of(): returns public Config

// Users can't directly instantiate ImmutableConfig

Builder type overshadowing

@Value.Style(overshadowImplementation = true)
@Value.Immutable
interface Data {
  String value();
}

// Builder types are also overshadowed:
// OLD: ImmutableData.Builder
// NEW: Data.Builder (interface nested in Data)

Data.Builder builder = Data.builder();  // Not ImmutableData.Builder
Data data = builder.value("test").build();

Limitations

1. Not all generators honor this:

Some Immutables add-on processors (like Mongo repository generator) may not respect overshadowImplementation.

2. Cast required for implementation-specific methods:

@Value.Style(overshadowImplementation = true)
@Value.Immutable
interface Person {
  String name();
}

Person person = Person.of("Alice");

// Can't access implementation-specific methods without cast:
// person.withName("Bob");  // Compile error if 'with' returns ImmutablePerson

// Need cast:
ImmutablePerson impl = (ImmutablePerson) person;
Person updated = impl.withName("Bob");

3. Type inference can be tricky:

@Value.Style(overshadowImplementation = true)
@Value.Immutable
interface Value<T> {
  T data();
}

// May need explicit type parameters:
Value<String> value = Value.<String>builder()
  .data("test")
  .build();

Example: Library API design

@Value.Style(
  overshadowImplementation = true,
  visibility = Value.Style.ImplementationVisibility.PACKAGE
)
@Value.Immutable
public interface Coordinate {
  double latitude();
  double longitude();

  static Coordinate of(double lat, double lon) {
    return ImmutableCoordinate.of(lat, lon);
  }

  static Builder builder() {
    return ImmutableCoordinate.builder();
  }

  interface Builder {
    Builder latitude(double lat);
    Builder longitude(double lon);
    Coordinate build();
  }
}

// Library users only see Coordinate interface:
public Coordinate findLocation(String address) {
  return Coordinate.of(37.7749, -122.4194);
}

Example (value-fixture/src/org/immutables/fixture/style/NestingClassOrBuilder.java:24):

@Value.Style(
  overshadowImplementation = true,
  visibility = Value.Style.ImplementationVisibility.PACKAGE
)

When to use

Use overshadowImplementation = true when:

  1. Public library API: Want clean, implementation-independent interface
  2. Testing: Need to mock value types in tests
  3. Encapsulation: Want to hide implementation details completely
  4. Future-proofing: May change implementation strategy later

Use default (false) when:

  1. Internal code: Implementation details don’t matter
  2. Performance-critical: Want to avoid interface dispatch overhead (minimal)
  3. Simplicity: Don’t need the abstraction

Performance considerations

Minimal overhead:

  • Interface method calls have negligible overhead on modern JVMs
  • JIT compiler typically inlines these calls
  • For most applications, the difference is immeasurable

See also: visibility, implementationNestedInBuilder, builderVisibility


packageGenerated

Type: String Default: "*" (same package as abstract value type)

Naming template controlling the package where generated immutable implementation classes are placed. Supports wildcards and patterns for flexible package organization.

Javadoc

From org.immutables.value.Value.Style.packageGenerated():

Note: It is expected that most generators will honor this style attribute, but it’s not guaranteed. When you generate derived classes in the same package (by default), then implementation could access and/or override package-private methods. If using a different package make sure to use public or protected access where needed, otherwise illegal access compilation errors will be flagged in the generated code.

Supported patterns

PatternExampleResult
"*" (default)com.example.modelcom.example.modelSame package
"*.generated"com.example.modelcom.example.model.generatedSubpackage
"*.impl"com.example.apicom.example.api.implImplementation subpackage
"com.myapp.gen"Any package → com.myapp.genFixed package
"*.immutables.impl"com.apicom.api.immutables.implNested subpackage

Default behavior (packageGenerated = ”*“)

package com.example.model;

@Value.Immutable
interface User {
  String name();
}

// Generated class in SAME package:
// com.example.model.ImmutableUser

Subpackage pattern (packageGenerated = “*.generated”)

@Value.Style(packageGenerated = "*.generated")
package com.example.model;

@Value.Immutable
interface User {
  String name();
}

// Generated class in subpackage:
// com.example.model.generated.ImmutableUser

Fixed package pattern (packageGenerated = “com.myapp.generated”)

@Value.Style(packageGenerated = "com.myapp.generated")
package com.example.api;

@Value.Immutable
interface Request {
  String id();
}

// Generated class in fixed package:
// com.myapp.generated.ImmutableRequest

Access considerations

Same package (default):

  • ✓ Can access package-private methods
  • ✓ Can override package-private methods
  • ✓ No visibility issues

Different package:

  • ✗ Cannot access package-private methods
  • ✗ Cannot override package-private methods
  • ⚠ Must use public/protected visibility

Example of access issue:

@Value.Style(packageGenerated = "*.generated")
package com.example;

@Value.Immutable
interface Data {
  String value();

  // Package-private method
  default void internalMethod() {
    // ...
  }
}

// Compilation ERROR!
// ImmutableData (in com.example.generated) cannot override
// package-private internalMethod() from Data (in com.example)

Solution: Use public/protected:

@Value.Style(packageGenerated = "*.generated")
package com.example;

@Value.Immutable
interface Data {
  String value();

  // Protected - accessible from different package
  default void internalMethod() {
    // ...
  }
}

// OK - ImmutableData can override protected method

Use cases

1. Separate generated code:

@Value.Style(packageGenerated = "*.generated")
package com.myapp.model;

// Abstract types in: com.myapp.model
// Generated impl in: com.myapp.model.generated

// Clean separation in IDE package view:
// com.myapp.model
//   ├── User.java
//   ├── Order.java
//   └── generated/
//       ├── ImmutableUser.java
//       └── ImmutableOrder.java

2. Implementation hiding:

@Value.Style(
  packageGenerated = "*.impl",
  visibility = Value.Style.ImplementationVisibility.PACKAGE  // Package-private impl
)
package com.myapp.api;

// Public API: com.myapp.api.User
// Hidden impl: com.myapp.api.impl.ImmutableUser (package-private)

3. Centralized generated code:

@Value.Style(packageGenerated = "com.myapp.generated.immutables")
package com.myapp.domain.user;

@Value.Immutable interface User { String name(); }

// All generated classes go to single package:
// com.myapp.generated.immutables.ImmutableUser
// com.myapp.generated.immutables.ImmutableOrder
// com.myapp.generated.immutables.ImmutableProduct

4. Maven/Gradle build integration:

@Value.Style(packageGenerated = "*.generated")
package com.example;

// Configure build to exclude generated packages from code coverage:
// <exclude>**/generated/**</exclude>

.gitignore patterns

When using subpackages for generated code:

# Ignore all generated subpackages
**/generated/

# Or specific pattern:
**/model/generated/
**/api/impl/

Multi-module projects

// Module 1: API (com.myapp.api)
@Value.Style(packageGenerated = "com.myapp.impl")
package com.myapp.api;

@Value.Immutable
public interface User {
  String id();
}

// Module 2: Implementation (com.myapp.impl)
// Contains: com.myapp.impl.ImmutableUser

// Module 3: Client (depends only on Module 1)
// Can use User interface without seeing ImmutableUser

Package naming best practices

DO:

  • ✓ Use consistent patterns across project
  • ✓ Keep generated code separate for clarity
  • ✓ Use .generated or .impl suffixes
  • ✓ Configure build tools to handle generated packages

DON’T:

  • ✗ Mix generated and hand-written code in same package
  • ✗ Use overly deep package nesting (*.a.b.c.generated)
  • ✗ Change package patterns mid-project (breaking change)

Interaction with IDE

IntelliJ IDEA:

Mark directory as Generated Sources Root:
  Project Structure → Modules → com.myapp.model.generated → Mark as: Generated Sources

Eclipse:

Build Path → Configure Build Path → Source tab
  → Add Folder → com/myapp/model/generated

Build configuration example

Maven:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <!-- Generated sources automatically added -->
    <generatedSourcesDirectory>
      ${project.build.directory}/generated-sources/annotations
    </generatedSourcesDirectory>
  </configuration>
</plugin>

Limitations

1. Not all generators honor this:

Some Immutables add-on processors might ignore packageGenerated and use default package.

2. Must be consistent across module:

Don’t mix different packageGenerated settings in same compilation unit - last one wins.

3. Reflection considerations:

If using reflection to find classes, must search in generated package:

// Wrong:
Class.forName("com.example.model.ImmutableUser");

// Right:
Class.forName("com.example.model.generated.ImmutableUser");

See also: visibility, typeImmutable


passAnnotations

Type: Class<? extends Annotation>[] Default: {} (empty array)

Array of annotation types to copy from the abstract value type and its attributes to the generated immutable implementation class. Useful for framework-specific annotations that need to be present on the implementation.

Javadoc

From org.immutables.value.Value.Style.passAnnotations():

This has a number of limitations, including that it’s not suited for ElementType.TYPE_USE annotations (annotations attached to types), only for method annotation. In many cases type-use annotation would be replicated automatically in derived signatures, but in case of type-use nullable annotations it’s the best to specify the nullable annotation of choice using fallbackNullableAnnotation so it will be used in generated code in all places where original annotations would not propagate automatically.

When passAnnotations is needed

Already handled automatically:

  • @Nullable annotations (all variants)
  • @javax.annotation.CheckForNull
  • Jackson annotations (@JsonProperty, @JsonIgnore, etc.)
  • @Inherited annotations (Java’s built-in inheritance)

Requires passAnnotations:

  • Framework-specific annotations (AWS DynamoDB, JPA, etc.)
  • Custom validation annotations
  • ORM mapping annotations
  • Non-inherited custom annotations

Default behavior (passAnnotations = {})

@MyCustomAnnotation  // NOT copied to implementation
@Value.Immutable
interface User {
  @FieldAnnotation  // NOT copied to implementation field
  String name();
}

// Generated implementation:
// (No @MyCustomAnnotation)
final class ImmutableUser implements User {
  // (No @FieldAnnotation)
  private final String name;
}

Copy annotations (passAnnotations = {…})

@Value.Style(passAnnotations = {MyCustomAnnotation.class, FieldAnnotation.class})
@MyCustomAnnotation  // Copied to implementation
@Value.Immutable
interface User {
  @FieldAnnotation  // Copied to implementation field
  String name();
}

// Generated implementation:
@MyCustomAnnotation  // ✓ Copied
final class ImmutableUser implements User {
  @FieldAnnotation  // ✓ Copied
  private final String name;
}

Use case: AWS DynamoDB annotations

import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.*;

@Value.Style(passAnnotations = {
  DynamoDbBean.class,
  DynamoDbPartitionKey.class,
  DynamoDbSortKey.class,
  DynamoDbAttribute.class
})
@Value.Immutable
@DynamoDbBean
interface UserRecord {
  @DynamoDbPartitionKey
  String userId();

  @DynamoDbSortKey
  String timestamp();

  @DynamoDbAttribute("user_name")
  String name();
}

// Generated implementation has all DynamoDB annotations:
@DynamoDbBean
final class ImmutableUserRecord implements UserRecord {
  @DynamoDbPartitionKey
  private final String userId;

  @DynamoDbSortKey
  private final String timestamp;

  @DynamoDbAttribute("user_name")
  private final String name;
}

Use case: JPA/Hibernate annotations

import jakarta.persistence.*;

@Value.Style(passAnnotations = {
  Entity.class,
  Table.class,
  Id.class,
  Column.class,
  Enumerated.class
})
@Value.Immutable
@Entity
@Table(name = "products")
interface Product {
  @Id
  Long id();

  @Column(name = "product_name", nullable = false, length = 100)
  String name();

  @Enumerated(EnumType.STRING)
  Category category();
}

Use case: Custom validation annotations

@Value.Style(passAnnotations = {
  ValidEmail.class,
  NonNegative.class
})
@Value.Immutable
interface RegistrationForm {
  @ValidEmail
  String email();

  @NonNegative
  int age();
}

// Validation framework sees annotations on implementation

Limitations

1. TYPE_USE annotations not supported:

@Value.Style(passAnnotations = {MyTypeUseAnnotation.class})
@Value.Immutable
interface Data {
  @MyTypeUseAnnotation String value();  // Won't work if TYPE_USE
}

// For TYPE_USE nullable annotations, use fallbackNullableAnnotation instead

2. Annotations must be runtime-retained:

// Won't work - SOURCE retention:
@Retention(RetentionPolicy.SOURCE)
@interface MyAnnotation {}

// Works - RUNTIME or CLASS retention:
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {}

3. Only copies to implementation, not builder:

@Value.Style(passAnnotations = {MyAnnotation.class})
@Value.Immutable
@MyAnnotation
interface Data {
  String value();
}

// Copied to ImmutableData
// NOT copied to ImmutableData.Builder

Interaction with @Inherited

// @Inherited annotation (no passAnnotations needed):
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface InheritedAnnotation {}

@InheritedAnnotation
@Value.Immutable
interface User {
  String name();
}

// @InheritedAnnotation automatically present on ImmutableUser

// Non-inherited annotation (needs passAnnotations):
@Retention(RetentionPolicy.RUNTIME)  // No @Inherited
@interface CustomAnnotation {}

@Value.Style(passAnnotations = {CustomAnnotation.class})
@CustomAnnotation
@Value.Immutable
interface User {
  String name();
}

Debugging: Check if annotation was copied

// At runtime:
boolean hasAnnotation = ImmutableUser.class.isAnnotationPresent(MyAnnotation.class);

// On field:
Field field = ImmutableUser.class.getDeclaredField("name");
boolean hasFieldAnnotation = field.isAnnotationPresent(FieldAnnotation.class);

Best practices

DO:

  • ✓ Only pass annotations that need to be on implementation
  • ✓ Use for framework-specific annotations (ORM, validation, etc.)
  • ✓ Document why annotations are being passed

DON’T:

  • ✗ Pass annotations already handled automatically (@Nullable, Jackson)
  • ✗ Pass TYPE_USE annotations (use fallbackNullableAnnotation)
  • ✗ Pass SOURCE-retention annotations

Example: Multiple frameworks

@Value.Style(passAnnotations = {
  // JPA
  Entity.class,
  Table.class,
  Id.class,
  Column.class,
  // Validation
  jakarta.validation.constraints.NotNull.class,
  jakarta.validation.constraints.Email.class,
  // Custom
  Audited.class,
  Versioned.class
})
package com.example.model;

Example source (issue #1567): Shows usage with AWS SDK DynamoDB annotations

See also: fallbackNullableAnnotation, additionalJsonAnnotations, headerComments


privateNoargConstructor

Type: boolean Default: false

When true, generates a private no-argument constructor in the immutable implementation class. Primarily used for serialization frameworks that require a no-arg constructor.

Javadoc

From org.immutables.value.Value.Style.privateNoargConstructor():

Generate a private no-argument constructor in generated code. Note that this property will be ignored if Immutable#singleton() returns true.

Default behavior (privateNoargConstructor = false)

@Value.Immutable
interface User {
  String name();
  int age();
}

// Generated implementation - NO no-arg constructor:
final class ImmutableUser {
  private final String name;
  private final int age;

  // Only parameterized constructor available
  private ImmutableUser(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

With private no-arg constructor (privateNoargConstructor = true)

@Value.Style(privateNoargConstructor = true)
@Value.Immutable
interface User {
  String name();
  int age();
}

// Generated implementation WITH private no-arg constructor:
final class ImmutableUser {
  private final String name;
  private final int age;

  // Private no-arg constructor added
  private ImmutableUser() {}

  // Regular parameterized constructor still present
  private ImmutableUser(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

Use cases

1. Serialization frameworks:

Some serialization libraries (like certain XML/JSON deserializers) require a no-arg constructor:

@Value.Style(privateNoargConstructor = true)
@Value.Immutable
@XmlRootElement
interface XmlData {
  String value();
}

// JAXB can use private no-arg constructor for deserialization

2. Reflection-based frameworks:

@Value.Style(privateNoargConstructor = true)
@Value.Immutable
interface ReflectiveBean {
  String property();
}

// Framework can instantiate via reflection:
// Constructor<?> ctor = ImmutableReflectiveBean.class.getDeclaredConstructor();
// ctor.setAccessible(true);
// Object instance = ctor.newInstance();

Ignored for singletons

@Value.Style(privateNoargConstructor = true)  // IGNORED
@Value.Immutable(singleton = true)
interface Config {
  @Value.Default
  default String setting() {
    return "default";
  }
}

// Singleton pattern doesn't use no-arg constructor
// Access via: ImmutableConfig.of()

Overridden by protectedNoargConstructor

@Value.Style(
  privateNoargConstructor = true,      // Ignored
  protectedNoargConstructor = true      // Takes precedence
)
@Value.Immutable
interface Data {
  String value();
}

// Generated constructor is PROTECTED, not private
final class ImmutableData {
  protected ImmutableData() {}  // Protected, not private
}

Interaction with prehash

@Value.Immutable(prehash = true)  // Precompute hashCode
@Value.Style(privateNoargConstructor = true)  // IGNORED with prehash
interface CachedHash {
  String value();
}

// prehash requires hashCode computation during construction
// No-arg constructor would create uninitialized object
// Therefore privateNoargConstructor is ignored

Why you might NOT want this

Security concerns:

  • Private constructor can still be accessed via reflection
  • Creates pathway to create partially initialized objects
  • Bypasses validation in normal constructors

Immutability concerns:

  • No-arg constructor creates objects with null/zero fields
  • Violates immutability guarantees
  • May cause NullPointerExceptions if fields accessed before initialization

Typical configuration for legacy interop

@Value.Style(
  privateNoargConstructor = true,
  finalInstanceFields = false  // Allow mutation after construction
)
@Value.Immutable
interface LegacyBean {
  String property();
}

// Enables JavaBean-style usage:
// 1. Create via no-arg constructor
// 2. Set fields via reflection/setters
// 3. Use as immutable value after initialization

Note: Overridden by protectedNoargConstructor if both are true.

See also: protectedNoargConstructor, finalInstanceFields


protectedNoargConstructor

Default: false

Generates protected no-argument constructor, mainly for reflective usage by advanced toolkits. Ignored if @Value.Immutable(singleton = true).

@Value.Style(protectedNoargConstructor = true)

Overrides privateNoargConstructor if both are true.

Example (value-fixture/src/org/immutables/fixture/style/NonFinalInstanceFields.java:21):

@Value.Style(
  finalInstanceFields = false,
  protectedNoargConstructor = true
)

See also: privateNoargConstructor


put

Default: "put*"

Naming template for builder methods that put a single entry to a map attribute.

@Value.Style(put = "add*Entry")
@Value.Immutable
interface Config {
  Map<String, String> properties();
}

// Generated usage:
ImmutableConfig.builder()
  .addPropertiesEntry("key", "value")
  .build();

See also: putAll, depluralize


putAll

Default: "putAll*"

Naming template for builder methods that put all entries from a map to a map attribute.

@Value.Style(putAll = "add*Entries")

See also: put


redactedMask

Default: "" (empty, attribute not shown in toString)

String to substitute for attributes marked with @Value.Redacted in generated toString().

@Value.Style(redactedMask = "***")
@Value.Immutable
interface Credentials {
  String username();
  @Value.Redacted
  String password();
}

// toString output: Credentials{username=john, password=***}

By default (empty string), redacted attributes don’t appear in toString() at all.

See also: @Value.Redacted


set

Default: "set*"

Naming template for “setter” methods in modifiable implementations.

@Value.Style(set = "update*")
@Value.Immutable
@Value.Modifiable
interface Config {
  String value();
}

// Generated usage:
ModifiableConfig config = ModifiableConfig.create();
config.updateValue("newValue");

Note: Do not confuse with init which is for builder methods.

See also: init, typeModifiable


setBuilder

Default: "*Builder"

Naming template for setting a nested builder. Only applies when attributeBuilderDetection is enabled. In strict mode, can only be called once.

@Value.Style(
  attributeBuilderDetection = true,
  setBuilder = "use*Builder"
)

See also: attributeBuilderDetection, getBuilder


setJacksonPropertyRequired

Default: true

Generates @JsonProperty(required=true) for mandatory attributes.

@Value.Style(setJacksonPropertyRequired = false)

See also: jacksonIntegration


stagedBuilder

Default: false

Generates staged (telescopic) builders providing compile-time safety for mandatory attributes. Each mandatory attribute becomes a stage in the builder, enforced at compile time.

@Value.Style(stagedBuilder = true)
@Value.Immutable
interface Person {
  String name();        // Mandatory stage 1
  int age();            // Mandatory stage 2
  Optional<String> nickname();  // Optional
}

// Generated usage - enforced order:
Person person = ImmutablePerson.builder()
  .name("John")    // Must be first
  .age(30)         // Must be second
  .nickname("JD")  // Optional attributes can be in any order
  .build();

Automatically enables strictBuilder.

Example (value-fixture/src/org/immutables/fixture/style/ExtendingAndStagedBuilder.java:21):

@Value.Style(
  stagedBuilder = true,
  visibility = Value.Style.ImplementationVisibility.PACKAGE,
  overshadowImplementation = true
)

Related issues:

  • #438 - Original feature request
  • #1377 - Cannot use with @Value.Enclosing

Note: May not play well with other functionality. May be auto-disabled in certain configurations like implementations nested in builder.

See also: strictBuilder


strictBuilder

Default: false

Generates forward-only strict builders. Prevents reinitialization errors:

  • Regular attributes: initializers can only be called once
  • Collections/maps: only additive initializers (no set/reset methods)
  • No from() method generated (error-inviting to reinitialize)
  • @Nullable and @Value.Default container attributes not supported
@Value.Style(strictBuilder = true)
@Value.Immutable
interface Config {
  String host();
  List<Integer> ports();
}

// Generated usage:
ImmutableConfig config = ImmutableConfig.builder()
  .host("localhost")
  .addPorts(8080)
  .addPorts(8081)
  .build();

// This would fail:
// builder.host("other");  // IllegalStateException: already set

Example (value-fixture/src/org/immutables/fixture/style/SpecifiedException.java:23):

@Value.Style(
  strictBuilder = true,
  throwForInvalidImmutableState = SampleRuntimeException.class
)

Related issues:

  • #1107 - Should check for duplicate map keys
  • #1314 - Optional wildcard handling
  • #1472 - With With* interface generation

See also: stagedBuilder, from


strictModifiable

Type: boolean

Default: true

Controls whether modifiable companion class accessors throw exceptions when accessing uninitialized mandatory attributes.

Javadoc (from Value.java:951-957)

Strict modifiable will refuse any accessor value (by throwing IllegalStateException) which is mandatory. Enabled by default. Set it to false and it will allow to get current field value even if not initialized (null for references, 0, false — for primitives).

Default behavior (strictModifiable = true)

When enabled (default), accessing an uninitialized mandatory attribute on a modifiable instance throws IllegalStateException:

@Value.Immutable
@Value.Modifiable
interface Person {
  String name();
  int age();
}

// Default strict behavior:
ModifiablePerson person = ModifiablePerson.create();
person.setAge(30);

// Accessing uninitialized mandatory attribute throws:
String name = person.getName();  // IllegalStateException: name is not set

This is the recommended behavior as it catches bugs early by preventing access to uninitialized state.

Customized behavior (strictModifiable = false)

When disabled, accessing uninitialized mandatory attributes returns default values instead of throwing:

@Value.Style(strictModifiable = false)
@Value.Immutable
@Value.Modifiable
interface Person {
  String name();
  int age();
  boolean active();
}

// Lenient behavior:
ModifiablePerson person = ModifiablePerson.create();
person.setAge(30);

// Accessing uninitialized attributes returns defaults:
String name = person.getName();    // Returns null (not exception)
boolean active = person.isActive(); // Returns false (primitive default)
int age = person.getAge();         // Returns 30 (was set)

Default values returned:

  • Reference types: null
  • Numeric primitives: 0 (byte, short, int, long, float, double)
  • boolean: false
  • char: '\0'

Use cases

Use strict mode (true) when:

  1. Catching initialization bugs - Force complete initialization before use
  2. Enforcing invariants - Ensure modifiable objects are fully populated
  3. API safety - Prevent NPEs by failing fast on uninitialized state
  4. Production code - Default safe behavior

Use lenient mode (false) when:

  1. Partial object construction - Building objects incrementally over time
  2. Optional initialization - Some attributes may remain unset
  3. Legacy compatibility - Matching existing JavaBean behavior
  4. Testing/prototyping - Quick experimentation without full initialization

Example: Partial object construction

@Value.Style(strictModifiable = false)
@Value.Immutable
@Value.Modifiable
interface FormData {
  String firstName();
  String lastName();
  String email();
  String phone();
}

// Collect form data incrementally:
ModifiableFormData form = ModifiableFormData.create();

// User fills in first name only:
form.setFirstName("John");

// Can safely check all fields without exceptions:
if (form.getFirstName() != null) { /* ... */ }
if (form.getLastName() != null) { /* ... */ }
if (form.getEmail() != null) { /* ... */ }
if (form.getPhone() != null) { /* ... */ }

// Convert to immutable when complete (will throw if mandatory fields missing):
FormData immutable = form.toImmutable();  // Throws if email/phone not set

Interaction with other attributes

Does NOT affect:

  • Builders - Builders have separate strict mode via strictBuilder
  • Immutable instances - Only affects modifiable companions
  • Conversion to immutable - toImmutable() still validates mandatory attributes

Works with:

Common mistakes

Mistake 1: Expecting null-safety with strictModifiable = false

@Value.Style(strictModifiable = false)
@Value.Immutable
@Value.Modifiable
interface Config {
  String value();
}

ModifiableConfig config = ModifiableConfig.create();
String upper = config.getValue().toUpperCase();  // NPE! getValue() returns null

Solution: Always check for null when using lenient mode:

String value = config.getValue();
if (value != null) {
  String upper = value.toUpperCase();
}

Mistake 2: Confusing with builder strictness

@Value.Style(strictModifiable = false)  // Only affects modifiables, NOT builders
@Value.Immutable
interface Config {
  String value();
}

// Builder still requires initialization:
ImmutableConfig config = ImmutableConfig.builder().build();  // Still throws!

Use strictBuilder to control builder strictness.

Best practices

  1. Keep default (true) for production - Strict mode catches bugs early
  2. Use lenient mode sparingly - Only when incremental construction is required
  3. Document lenient usage - Make it clear why strict mode was disabled
  4. Validate before conversion - Check all required fields before calling toImmutable()
  5. Consider Optional attributes - Use Optional<T> for truly optional fields instead of lenient mode

Limitations

  • Only affects modifiable companions (classes with @Value.Modifiable)
  • Does not affect builder behavior
  • Does not change validation when converting to immutable
  • Cannot customize exception type (always IllegalStateException in strict mode)

See also: typeModifiable, strictBuilder, toImmutable, @Value.Modifiable


throwForInvalidImmutableState

Type: Class<? extends RuntimeException>

Default: IllegalStateException.class

Specifies the runtime exception type to throw when an immutable object cannot be built due to missing mandatory attributes or other invalid state conditions.

Javadoc (from Value.java:1344-1363)

Runtime exception to throw when an immutable object is in an invalid state. I.e. when some mandatory attributes are missing and immutable object cannot be built. The runtime exception class must have a constructor that takes a single string, otherwise there will be compile-time error in the generated code.

The default exception type is IllegalStateException. In case if specified exception type have public constructor taking array of strings (can be varargs), then missing parameter names will be passed to that constructor. Otherwise, string constructor is always expected to be present to take formatted error message. It is always advisable have string constructor even in the presence of attribute names array constructor as some additional generators might use string constructor for reporting other invalid state issues.

Technically we allow exception class to be checked (non-runtime), but not all processor features might be generated correctly (they may not expect). So use checked exception only if this work with your set of use-cases: there is no guarantee that it will be ever supported in all processor components/templates.

Default behavior (IllegalStateException)

By default, IllegalStateException is thrown when mandatory attributes are missing:

@Value.Immutable
interface Person {
  String name();
  int age();
}

// Missing mandatory attributes:
ImmutablePerson person = ImmutablePerson.builder()
  .age(30)
  .build();  // IllegalStateException: Cannot build Person, some of required attributes are not set [name]

Customized behavior (Custom exception class)

You can specify a custom exception type to provide domain-specific error handling:

// Define custom exception with required constructors:
public class ValidationException extends RuntimeException {
  // Required: String constructor for formatted messages
  public ValidationException(String message) {
    super(message);
  }

  // Optional: String[] constructor for missing parameter names
  public ValidationException(String... missingParams) {
    super("Missing required fields: " + String.join(", ", missingParams));
  }
}

@Value.Style(throwForInvalidImmutableState = ValidationException.class)
@Value.Immutable
interface Person {
  String name();
  String email();
  int age();
}

// Now throws ValidationException instead of IllegalStateException:
try {
  ImmutablePerson person = ImmutablePerson.builder()
    .age(30)
    .build();
} catch (ValidationException e) {
  // Custom exception: "Missing required fields: name, email"
  logger.error("Validation failed", e);
}

Constructor requirements

Mandatory constructor: public ExceptionType(String message)

  • Must be present for formatted error messages
  • Used by most generator components

Optional constructor: public ExceptionType(String... missingParams) or public ExceptionType(String[] missingParams)

  • If present, receives array of missing parameter names
  • Allows custom formatting of missing field messages
  • Still recommended to have String constructor as fallback
public class BuildException extends RuntimeException {
  // Mandatory: Used by various generators
  public BuildException(String message) {
    super(message);
  }

  // Optional: Receives missing parameter names
  public BuildException(String[] missingParams) {
    super("Build failed. Missing: " + String.join(", ", missingParams));
  }
}

Use cases

1. Domain-specific exceptions

public class OrderValidationException extends RuntimeException {
  public OrderValidationException(String message) {
    super(message);
  }
}

@Value.Style(throwForInvalidImmutableState = OrderValidationException.class)
@Value.Immutable
interface Order {
  String customerId();
  List<String> items();
  BigDecimal total();
}

2. Exception hierarchy integration

// Integrate with existing exception hierarchy:
public class DomainException extends RuntimeException {
  public DomainException(String message) {
    super(message);
  }
}

public class EntityBuildException extends DomainException {
  public EntityBuildException(String message) {
    super(message);
  }

  public EntityBuildException(String[] missingFields) {
    super("Entity validation failed: " + Arrays.toString(missingFields));
  }
}

@Value.Style(throwForInvalidImmutableState = EntityBuildException.class)

3. Unified validation exception

// Use same exception for both state and null violations:
@Value.Style(
  throwForInvalidImmutableState = IllegalArgumentException.class,
  throwForNullPointer = IllegalArgumentException.class
)

When exceptions are thrown

Builder context:

  • Calling build() with missing mandatory attributes
  • Calling build() when @Value.Check validation fails
  • Invalid state detected during construction

Factory method context:

  • Missing parameters to of() factory methods (with @Value.Parameter)

Modifiable context:

  • Calling toImmutable() on modifiable with missing mandatory attributes

Interaction with other attributes

Works with:

  • throwForNullPointer - Separate exception for null violations
  • validationMethod - Controls what validation is performed
  • strictBuilder - May throw earlier when attributes set multiple times
  • @Value.Check - Custom validation throwing same exception type

Does NOT affect:

  • Runtime behavior of immutable instances (only construction failures)
  • Exceptions from user code in @Value.Default, @Value.Derived, @Value.Check methods

Common mistakes

Mistake 1: Missing required String constructor

// BAD: Missing String constructor
public class CustomException extends RuntimeException {
  public CustomException(String[] params) {
    super(Arrays.toString(params));
  }
  // Missing: public CustomException(String message) { ... }
}

@Value.Style(throwForInvalidImmutableState = CustomException.class)
// Compile error in generated code!

Solution: Always include String constructor:

public class CustomException extends RuntimeException {
  public CustomException(String message) {
    super(message);
  }

  public CustomException(String[] params) {
    super(Arrays.toString(params));
  }
}

Mistake 2: Using checked exceptions

// DISCOURAGED: Checked exceptions not fully supported
public class CheckedException extends Exception {
  public CheckedException(String message) {
    super(message);
  }
}

@Value.Style(throwForInvalidImmutableState = CheckedException.class)
// May work in some cases, but not guaranteed across all features

Solution: Use RuntimeException subclasses.

Mistake 3: Forgetting to handle custom exception

@Value.Style(throwForInvalidImmutableState = ValidationException.class)
@Value.Immutable
interface Config {
  String value();
}

// Forgotten to catch ValidationException:
Config config = ImmutableConfig.builder().build();  // Throws ValidationException

Solution: Handle the custom exception type:

try {
  Config config = ImmutableConfig.builder().build();
} catch (ValidationException e) {
  // Handle validation failure
}

Best practices

  1. Use RuntimeException subclasses - Checked exceptions not fully supported
  2. Always provide String constructor - Required for all generator components
  3. Document exception conditions - Make it clear when custom exception is thrown
  4. Consider exception hierarchy - Integrate with existing application exceptions
  5. Pair with throwForNullPointer - Use consistent exception types across validation
  6. Test custom exceptions - Verify exception messages and types in tests

Example from Immutables codebase

Example (value-fixture/src/org/immutables/fixture/style/SpecifiedException.java:23):

@Value.Style(
  strictBuilder = true,
  throwForInvalidImmutableState = SampleRuntimeException.class,
  throwForNullPointer = SampleRuntimeException.class
)
public @interface SpecifiedExceptionStyle {}

This style uses a unified custom exception for all validation failures.

Advanced: Parameterized exception with context

public class EntityValidationException extends RuntimeException {
  private final List<String> missingFields;

  public EntityValidationException(String message) {
    super(message);
    this.missingFields = Collections.emptyList();
  }

  public EntityValidationException(String[] missingFields) {
    super("Missing fields: " + String.join(", ", missingFields));
    this.missingFields = Arrays.asList(missingFields);
  }

  public List<String> getMissingFields() {
    return missingFields;
  }
}

@Value.Style(throwForInvalidImmutableState = EntityValidationException.class)
@Value.Immutable
interface User {
  String username();
  String email();
  String password();
}

// Usage with structured error handling:
try {
  User user = ImmutableUser.builder()
    .username("john")
    .build();
} catch (EntityValidationException e) {
  if (e.getMissingFields().contains("email")) {
    // Handle missing email specifically
  }
}

Limitations

  • Must be a RuntimeException subclass (checked exceptions not fully supported)
  • String constructor is mandatory
  • Cannot customize exception message format beyond constructor logic
  • Does not affect exceptions from user-defined @Value.Check, @Value.Default, or @Value.Derived methods
  • Same exception type used for all invalid state conditions (cannot differentiate between different validation failures)

See also: throwForNullPointer, validationMethod, strictBuilder, @Value.Check, buildOrThrow


throwForNullPointer

Type: Class<? extends RuntimeException>

Default: NullPointerException.class

Specifies the runtime exception type to throw when null references are passed to non-nullable parameters or occur in arrays/containers that must not contain nulls.

Javadoc (from Value.java:1366-1375)

Runtime exception to throw when null reference is passed to non-nullable parameter or occurred in array/container that must not contain nulls. It is expected that the exception will have public constructor receiving string as message/parameter name. The default is NullPointerException and the calls are usually delegated to Objects.requireNonNull(Object) or similar utility throwing NullPointerException.

Default behavior (NullPointerException)

By default, NullPointerException is thrown when null values are passed to non-nullable parameters:

@Value.Immutable
interface Person {
  String name();
  List<String> tags();
}

// Null passed to non-nullable attribute:
ImmutablePerson person = ImmutablePerson.builder()
  .name(null)  // NullPointerException: name
  .build();

// Null in collection:
ImmutablePerson person2 = ImmutablePerson.builder()
  .name("John")
  .addTags("a", null, "b")  // NullPointerException: tags element
  .build();

The exception is typically thrown via Objects.requireNonNull() or similar null-checking utilities.

Customized behavior (Custom exception class)

You can specify a custom exception type for domain-specific null handling:

// Define custom exception:
public class InvalidInputException extends RuntimeException {
  public InvalidInputException(String message) {
    super(message);
  }
}

@Value.Style(throwForNullPointer = InvalidInputException.class)
@Value.Immutable
interface Person {
  String name();
  String email();
}

// Now throws InvalidInputException instead of NullPointerException:
try {
  ImmutablePerson person = ImmutablePerson.builder()
    .name(null)
    .email("test@example.com")
    .build();
} catch (InvalidInputException e) {
  // Custom exception for null validation
  logger.warn("Invalid input", e);
}

When null exceptions are thrown

1. Null passed to non-nullable builder setters:

@Value.Immutable
interface Config {
  String value();
}

ImmutableConfig.builder()
  .value(null)  // Throws throwForNullPointer exception
  .build();

2. Null in collection/array elements:

@Value.Immutable
interface Data {
  List<String> items();
}

ImmutableData.builder()
  .addItems("a", null, "b")  // Throws throwForNullPointer exception
  .build();

3. Null passed to factory methods (@Value.Parameter):

@Value.Immutable
interface Pair {
  @Value.Parameter String first();
  @Value.Parameter String second();
}

ImmutablePair.of(null, "value");  // Throws throwForNullPointer exception

4. Null in map keys or values (when not explicitly nullable):

@Value.Immutable
interface Config {
  Map<String, String> properties();
}

ImmutableConfig.builder()
  .putProperties("key", null)  // Throws throwForNullPointer exception
  .build();

Attributes NOT affected by throwForNullPointer

Null checks are NOT performed for:

  • @Nullable attributes - Explicitly allow null values
  • Optional<T> attributes - null converted to Optional.empty()
  • @Value.Default attributes - Default value used instead of null
  • Collection attributes when using addAll(null) - Treats null collection as empty
@Value.Immutable
interface Person {
  @Nullable String nickname();        // null allowed
  Optional<String> email();           // null → Optional.empty()
  @Value.Default String role() {      // null → "user"
    return "user";
  }
  List<String> tags();                // addAllTags(null) → no exception
}

ImmutablePerson person = ImmutablePerson.builder()
  .nickname(null)     // OK, @Nullable
  .email(null)        // OK, converted to Optional.empty()
  .role(null)         // OK, default used
  .addAllTags(null)   // OK, treated as empty
  .build();

Use cases

1. Domain-specific null validation

public class ValidationException extends RuntimeException {
  public ValidationException(String message) {
    super("Validation failed: " + message);
  }
}

@Value.Style(throwForNullPointer = ValidationException.class)
@Value.Immutable
interface Order {
  String orderId();
  String customerId();
  List<String> items();
}

2. Unified exception type with throwForInvalidImmutableState

// Use same exception for both null and missing attribute violations:
@Value.Style(
  throwForNullPointer = IllegalArgumentException.class,
  throwForInvalidImmutableState = IllegalArgumentException.class
)

3. Consistent error handling across application

// Application-wide exception hierarchy:
public class DomainException extends RuntimeException {
  public DomainException(String message) {
    super(message);
  }
}

public class NullValueException extends DomainException {
  public NullValueException(String message) {
    super("Null value not allowed: " + message);
  }
}

@Value.Style(throwForNullPointer = NullValueException.class)

Interaction with other attributes

Works with:

Does NOT affect:

  • Null checks in user code (@Value.Check, @Value.Derived, @Value.Default)
  • Optional attribute handling
  • Collections when using addAll(null) or putAll(null)

Common mistakes

Mistake 1: Expecting exception for @Nullable attributes

@Value.Style(throwForNullPointer = IllegalArgumentException.class)
@Value.Immutable
interface Config {
  @Nullable String value();  // Explicitly nullable
}

// No exception thrown:
Config config = ImmutableConfig.builder()
  .value(null)  // OK, @Nullable allows null
  .build();

@Nullable explicitly permits null values regardless of throwForNullPointer setting.

Mistake 2: Missing String constructor

// BAD: No String constructor
public class CustomException extends RuntimeException {
  public CustomException() {
    super();
  }
}

@Value.Style(throwForNullPointer = CustomException.class)
// Compile error in generated code!

Solution: Always include String constructor:

public class CustomException extends RuntimeException {
  public CustomException(String message) {
    super(message);
  }
}

Mistake 3: Confusing with Optional.ofNullable()

@Value.Immutable
interface Config {
  Optional<String> value();
}

// This does NOT throw:
Config config = ImmutableConfig.builder()
  .value((String) null)  // Converts to Optional.empty(), no exception
  .build();

For Optional attributes, null is converted to Optional.empty() rather than throwing.

Best practices

  1. Use RuntimeException subclasses - Checked exceptions not supported
  2. Pair with throwForInvalidImmutableState - Use consistent exception types
  3. Provide String constructor - Mandatory for exception class
  4. Document null policies - Make it clear what can and cannot be null
  5. Use @Nullable explicitly - Don’t rely on implicit null handling
  6. Test null validation - Verify exceptions thrown with correct types

Example: Application-wide validation exceptions

// Define application exception hierarchy:
public class AppValidationException extends RuntimeException {
  public AppValidationException(String message) {
    super(message);
  }
}

// Meta-annotation for consistent validation:
@Value.Style(
  throwForNullPointer = AppValidationException.class,
  throwForInvalidImmutableState = AppValidationException.class
)
@Target({ElementType.PACKAGE, ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface AppStyle {}

// Apply to package:
@AppStyle
package com.example.domain;

// All value objects in package now use unified validation:
@Value.Immutable
interface User {
  String username();  // Null throws AppValidationException
  String email();     // Null throws AppValidationException
}

@Value.Immutable
interface Order {
  String orderId();   // Null throws AppValidationException
  List<String> items(); // Null element throws AppValidationException
}

Advanced: Detailed null validation messages

public class DetailedNullException extends RuntimeException {
  private final String fieldName;

  public DetailedNullException(String fieldName) {
    super("Required field '" + fieldName + "' cannot be null");
    this.fieldName = fieldName;
  }

  public String getFieldName() {
    return fieldName;
  }
}

@Value.Style(throwForNullPointer = DetailedNullException.class)
@Value.Immutable
interface Order {
  String customerId();
  String orderId();
  List<String> items();
}

// Usage with structured error handling:
try {
  ImmutableOrder order = ImmutableOrder.builder()
    .customerId(null)
    .orderId("ORD-123")
    .build();
} catch (DetailedNullException e) {
  if ("customerId".equals(e.getFieldName())) {
    // Handle missing customer ID specifically
    logger.error("Customer ID is required", e);
  }
}

Limitations

  • Must be a RuntimeException subclass
  • String constructor is mandatory
  • Cannot customize null check logic (always uses Objects.requireNonNull() pattern)
  • Does not affect null checks in user code (@Value.Check, @Value.Derived, etc.)
  • Cannot differentiate between null attribute value vs null collection element
  • Does not affect @Nullable attributes, Optional attributes, or default attributes

See also: throwForInvalidImmutableState, validationMethod, nullableAnnotation, @Nullable


toBuilder

Default: "" (empty, method not generated)

Naming template for method on immutable instance that returns new builder with values pre-initialized from instance (equivalent to builder().from(this)).

@Value.Style(toBuilder = "toBuilder")
@Value.Immutable
interface Config {
  String host();
  int port();
}

// Generated usage:
Config config = ImmutableConfig.builder()
  .host("localhost")
  .port(8080)
  .build();

Config modified = config.toBuilder()
  .port(8081)  // Change just the port
  .build();

Note:

  • Experimental feature, may change in future releases
  • Not compatible with strictBuilder
  • When enabled, abstract method Builder toBuilder() in abstract value type is not considered an attribute

See also: from


toImmutable

Default: "toImmutable"

Method name to convert modifiable instance to immutable.

@Value.Style(toImmutable = "freeze")
@Value.Immutable
@Value.Modifiable
interface Config {
  String value();
}

// Generated usage:
ModifiableConfig modifiable = ModifiableConfig.create()
  .setValue("test");
Config immutable = modifiable.freeze();

See also: copyOf, typeModifiable


transientDerivedFields

Default: true

When true, backing fields for @Value.Derived attributes are marked as transient (unless type is Serializable using regular Java serialization).

@Value.Style(transientDerivedFields = false)

Set to false if derived fields should participate in persistence/serialization. Lazy attributes (@Value.Lazy) are always transient regardless.

Example (value-fixture/src/org/immutables/fixture/style/TransientDerivedFields.java): Demonstrates both true and false with serialization

See also: @Value.Derived, @Value.Lazy


typeAbstract

Default: "Abstract*"

Patterns to detect base/raw type name from abstract value type name. If none match, raw type name is taken literally.

@Value.Style(typeAbstract = {"*Def", "Base*"})
@Value.Immutable
interface PersonDef {  // Detected as "Person"
  String name();
}

// Generated: ImmutablePerson (not ImmutablePersonDef)

Example (value-fixture/src/org/immutables/fixture/style/AbstractValueNamingDetected.java:25):

@Value.Style(
  typeAbstract = "Abstract*",
  typeImmutable = "*"
)

Note: This is a detection pattern, not a formatting pattern.

See also: typeImmutable


typeBuilder

Default: "Builder"

Generated builder class name template.

@Value.Style(typeBuilder = "Maker")
@Value.Immutable
interface Value {
  String data();
}

// Generated: ImmutableValue.Maker

See also: builder, typeInnerBuilder


typeImmutable

Default: "Immutable*"

Naming template for immutable implementation type using base/raw name (detected via typeAbstract).

@Value.Style(
  typeAbstract = "*Def",
  typeImmutable = "*"  // No prefix/suffix
)
@Value.Immutable
interface PersonDef {
  String name();
}

// Generated: Person (not ImmutablePersonDef)

Example (value-fixture/src/org/immutables/fixture/style/Tuple.java:21):

@Value.Style(typeImmutable = "*Tuple")

See also: typeAbstract, typeImmutableNested


typeImmutableEnclosing

Default: "Immutable*"

Umbrella nesting class name when using @Value.Enclosing.

@Value.Style(typeImmutableEnclosing = "Generated*")
@Value.Enclosing
class Models {
  @Value.Immutable
  interface Person {
    String name();
  }
}

// Generated: GeneratedModels.Person

See also: typeImmutableNested, @Value.Enclosing


typeImmutableNested

Default: "*"

Immutable class name when generated under @Value.Enclosing umbrella class.

@Value.Style(
  typeImmutableEnclosing = "Immutable*",
  typeImmutableNested = "*"  // No prefix
)
@Value.Enclosing
class Models {
  @Value.Immutable
  interface Person { String name(); }
}

// Generated: ImmutableModels.Person (not ImmutableModels.ImmutablePerson)

See also: typeImmutableEnclosing, @Value.Enclosing


typeInnerBuilder

Default: "Builder"

Inner builder class name pattern for matching existing builder to extend/super.

// Abstract value type:
@Value.Immutable
interface Value {
  String data();

  class Builder extends ImmutableValue.Builder {}
}

@Value.Style(typeInnerBuilder = "Maker")
// Would match:
// class Maker extends ImmutableValue.Maker {}

See also: typeBuilder


typeInnerModifiable

Default: "Modifiable"

Inner modifiable class name for matching existing modifiable to extend.

@Value.Style(typeInnerModifiable = "Mutable")

See also: typeModifiable


typeModifiable

Default: "Modifiable*"

Naming template for modifiable companion class.

@Value.Style(typeModifiable = "Mutable*")
@Value.Immutable
@Value.Modifiable
interface Config {
  String value();
}

// Generated: MutableConfig

See also: @Value.Modifiable, typeInnerModifiable


typeWith

Default: "With*"

Naming template for detecting/generating “with” interface for sandwich-style.

@Value.Immutable
interface Value extends WithValue {  // WithValue not yet generated
  String data();
}

// Generated interface: WithValue
// Contains: WithValue withData(String data);

See also: with


underrideEquals

Default: "" (empty, not used)

Method name for custom equals implementation in interface. Since interfaces cannot override Object.equals() as default method, specify a method name here that will be wired into generated equals().

@Value.Style(underrideEquals = "equalTo")
@Value.Immutable
interface Value {
  String data();

  default boolean equalTo(Object other) {
    // Custom equals logic
    if (!(other instanceof Value)) return false;
    Value that = (Value) other;
    return this.data().equalsIgnoreCase(that.data());  // Case-insensitive
  }
}

// Generated equals() delegates to equalTo()

Method can be default or static. Static version takes instance parameter.

Example (value-fixture/src/org/immutables/fixture/style/UnderrideObjectMethods.java:26):

@Value.Style(
  underrideEquals = "equalTo",
  underrideHashCode = "hash",
  underrideToString = "stringify"
)

See also: underrideHashCode, underrideToString


underrideHashCode

Default: "" (empty, not used)

Method name for custom hashCode implementation in interface. Since interfaces cannot override Object.hashCode() as default method, specify a method name here that will be wired into generated hashCode().

@Value.Style(underrideHashCode = "hash")
@Value.Immutable
interface Value {
  String data();

  default int hash() {
    return data().toLowerCase().hashCode();  // Case-insensitive hash
  }
}

// Generated hashCode() delegates to hash()

Method can be default or static. Static version takes instance parameter.

See also: underrideEquals, includeHashCode


underrideToString

Default: "" (empty, not used)

Method name for custom toString implementation in interface. Since interfaces cannot override Object.toString() as default method, specify a method name here that will be wired into generated toString().

@Value.Style(underrideToString = "stringify")
@Value.Immutable
interface Value {
  String data();

  default String stringify() {
    return "CustomValue[" + data() + "]";
  }
}

// Generated toString() delegates to stringify()

Method can be default or static. Static version takes instance parameter.

See also: underrideEquals, delegateToString


unsafeDefaultAndDerived

Default: false

When true, reverts to pre-2.1 unsafe behavior for default and derived attributes. In unsafe mode, initializers cannot refer to other default/derived attributes as initialization order is not guaranteed.

@Value.Style(unsafeDefaultAndDerived = true)

By default (false), safe code is generated that detects initialization cycles and throws IllegalStateException at runtime if cycles exist.

Note: Enabling this provides simpler code with less overhead but requires careful attention to avoid undefined behavior.

See also: @Value.Default, @Value.Derived


unset

Default: "unset*"

Naming template for unsetting attribute in modifiable implementations.

@Value.Style(unset = "clear*")
@Value.Immutable
@Value.Modifiable
interface Config {
  Optional<String> value();
}

// Generated usage:
ModifiableConfig config = ModifiableConfig.create();
config.setValue("test");
config.clearValue();  // Unsets the value

See also: set, clear


validationMethod

Default: ValidationMethod.SIMPLE

Specifies validation method for builders.

Options:

  • NONE - Disables null and mandatory checks. Missing primitives get zero values (false, 0, '\0'). Object references are null. Optional/default/collection attributes still get appropriate defaults.

  • MANDATORY_ONLY - No null checks, but verifies all non-@Default and non-@Nullable attributes are provided (even if with null values for objects).

  • SIMPLE - Classic fail-fast null-hostile behavior. Verifies non-null attributes provided. Works best in most cases.

  • VALIDATION_API - Uses Bean Validation API (JSR 303). Disables null checks in favor of @javax.validation.constraints.NotNull. Creates static validator per object.

@Value.Style(validationMethod = ValidationMethod.NONE)
@Value.Immutable
interface Partial {
  String value();
  int count();
}

// With NONE:
Partial p = ImmutablePartial.builder().build();  // OK
// p.value() returns null, p.count() returns 0

Example (value-fixture/src/org/immutables/fixture/style/NoValidation.java:21):

@Value.Style(
  validationMethod = ValidationMethod.NONE,
  jdkOnly = true
)

Note: For better control with JSR 303 or custom validation, use @Value.Check method in base abstract class/interface. See issue #26.


visibility

Default: ImplementationVisibility.SAME

Controls visibility of generated immutable implementation class.

Options:

  • PUBLIC - Always public
  • SAME - Same as abstract value type
  • SAME_NON_RETURNED - Same visibility but not returned from build/factory (deprecated, use with overshadowImplementation)
  • PACKAGE - Package-private
  • PRIVATE - Private (only when builder enabled or nested in enclosing type)
@Value.Style(visibility = ImplementationVisibility.PACKAGE)
public interface Config {
  String value();
}

// ImmutableConfig is package-private but Config interface is public

Example (value-fixture/src/org/immutables/fixture/style/HiddenImplementation.java:23):

@Value.Style(visibility = ImplementationVisibility.PRIVATE)

See also: visibilityString, builderVisibility


visibilityString

Default: "" (empty, uses visibility)

String-based alternative to visibility to avoid javac warnings (see issue #291).

@Value.Style(visibilityString = "PACKAGE")

When specified, overrides visibility.

Example (value-fixture/src/org/immutables/fixture/style/MoreVisibleImplementationAsString.java:22):

@Value.Style(
  visibilityString = "PUBLIC",
  builderVisibilityString = "PACKAGE"
)

See also: visibility


weakInterning

Default: false

When enabled, value types with @Value.Immutable(intern=true) use weak interning (via WeakReference).

@Value.Style(weakInterning = true)
@Value.Immutable(intern = true)
interface Value {
  String data();
}

// Instances weakly interned - can be GC'd when no strong references exist

Example (value-fixture/src/org/immutables/fixture/style/WeakInterningStyle.java:21):

@Value.Style(weakInterning = true)
@Value.Style(weakInterning = true, jdkOnly = true)  // JDK variant

See also: @Value.Immutable(intern = true)


with

Default: "with*"

Naming template for modify-by-copy “with” methods.

@Value.Style(with = "copyWith*")
@Value.Immutable
interface Config {
  String host();
  int port();
}

// Generated usage:
Config config = ImmutableConfig.builder()
  .host("localhost")
  .port(8080)
  .build();

Config modified = config.copyWithPort(8081);  // Modified copy

Example (value-fixture/src/org/immutables/fixture/style/PackageStyle.java:23):

@Value.Style(with = "copyWith*")

See also: init, withUnaryOperator, forceEqualsInWithers


withUnaryOperator

Default: "" (empty, feature disabled)

Naming template for modify-by-copy methods that accept UnaryOperator to transform attribute before constructing copy.

@Value.Style(withUnaryOperator = "with*Mapped")
@Value.Immutable
interface Config {
  String value();
  List<String> items();
  Optional<String> optional();
}

// Generated usage:
Config config = ImmutableConfig.builder()
  .value("test")
  .addItems("a", "b")
  .optional("opt")
  .build();

Config mapped = config
  .withValueMapped(String::toUpperCase)      // Transform value
  .withItemsMapped(s -> s + "!")             // Transform each item
  .withOptionalMapped(String::toUpperCase);  // Transform optional content

Transforms values, optional values, and collection elements. Currently, supports JDK Optional only.

Note: Feature is disabled by default unless template specified. Can be same as with (e.g., "with*") as there should be no overload collisions, in theory.

See also: with