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:
See
addDefault: "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
addAllDefault: "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.
addAllBuilderDefault: "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
addBuilderDefault: "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
additionalJsonAnnotationsDefault: {} (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
additionalStrictContainerConstructorDefault: 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:
List<T> as parameter (matches the declared type exactly)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:
stagedBuilder and generic types in version 2.11.5Note: 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
allowedClasspathAnnotationsDefault: {} (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.ImmutableThis 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:
java.lang annotations like @Override, @Deprecated, @SuppressWarnings are always applied where supported, regardless of this configurationjavax.annotation.* and java.annotation.processing.* annotations ARE configurable by this attributepassAnnotations) are not regulated by this propertyAlternative 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:
javax.annotation.Generated and the interaction between allowedClasspathAnnotations and META-INF inhibit mechanismjakarta style attribute)Example (value-fixture/src/org/immutables/fixture/style/LightOnAnnotations.java:15):
@Value.Style(
generateSuppressAllWarnings = false,
allowedClasspathAnnotations = {Override.class}
)
See also: generateSuppressAllWarnings, jakarta, passAnnotations
allMandatoryParametersDefault: 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:
@Value.Default@Nullable or similar nullable annotationsOptional<T>) or collection types (which default to empty)@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:
@Builder.Constructor does not support allMandatoryParameters and other style parametersallParametersSee also: allParameters, of, @Value.Parameter
allParametersDefault: 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:
allMandatoryParameters@Value.Immutable(builder = false) in the defaultsdeepImmutablesDetection to create shortcut initializers (see related issues)Related Issues:
deepImmutablesDetectiondeepImmutablesDetectiondeepImmutablesDetectionExample (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
alwaysPublicInitializersDefault: 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):
publicWhen false:
@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:
stagedBuilder is enabled. Staged builders generate stage interfaces which require public methods to function correctly, so initializers will be public regardless of this setting.Use cases:
Related Issues:
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
attributeBuilderDefault: ["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 Builder builder() or static SomeBuilder from()"new": Indicates that a public no-arg constructor should be used for builder instantiationPattern 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:
attributeBuilderDetection is trueUse 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
attributeBuilderDetectionDefault: 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 instanceGenerated API for collection of children:
BuilderT add*Builder() - Add a new builder to the collection and return it for configurationParentT addAll*Builder(Iterable<BuilderT>) - Add multiple builder instances from an iterableParentT addAll*Builder(BuilderT...) - Add multiple builder instances via varargsList<BuilderT> *Builders() - Get the list of all builders added so farHow 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:
*Builder(BuilderT builder)*Builder() getter can be called multiple times, returning the same builder instanceBuilder requirements: For a type to be recognized as having a discoverable builder:
attributeBuilder patternsbuild() method returning the immutable typePerformance 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
attributelessSingletonDefault: 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:
of() per instance style) provides access to the singletonWhen false (default):
@Value.Immutable(singleton = true) is required for singleton generation@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:
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
beanFriendlyModifiablesDefault: 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):
this for method chainingobj.setName("x").setValue(42)When true:
voidobj.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:
Use cases:
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
buildDefault: "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 variantMethod behavior: The build method:
throwForInvalidImmutableState exception if validation fails@Value.Check validation methodsExample (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
buildOrThrowDefault: "" (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:
Function<List<String>, RuntimeException> parameter (Java 8+) or Function<List<String>, RuntimeException> from Guava (Java 7)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:
Function type supportRuntimeException (or subclass)Use cases:
ValidationException, ConfigurationException)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
builderDefault: "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:
@Value.Check (validation methods)@Value.Immutable(singleton = true)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:
newBuildertypeBuilderExample (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
builderToStringDefault: 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:
<not set>)When disabled (default):
toString() method is generated on builderstoString() 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:
Output format: The exact format may vary but typically includes:
<not set>) for uninitialized mandatory attributesPerformance 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
builderVisibilityDefault: 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 typePACKAGE - 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:
Interaction with other features:
visibility is more restrictive than builder visibility (e.g., PRIVATE impl with PUBLIC builder), overshadowImplementation is automatically enabledimplementationNestedInBuilder, the builder becomes the top-level class and its visibility becomes more importantRelated Issues:
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
builderVisibilityStringDefault: "" (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:
builderVisibility by default for type safetybuilderVisibilityString if you encounter compiler warnings or issuesNote: This is purely a convenience/compatibility feature. The generated code is identical whether you use the enum or string version.
See also: builderVisibility, visibilityString
builtinContainerAttributesDefault: 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.of() handlingadd*(), addAll*() methodsput*(), putAll*() methodsWhen 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:
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
canBuildDefault: "" (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:
true if all mandatory attributes are set and build() would succeedfalse if any mandatory attributes are missing and build() would throw an exception@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:
Related Issues:
isInitialized vs canBuild on builderscanBuild in nested interfacesExample (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
clearDefault: "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:
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:
Optional.empty()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 clearedUse 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
clearBuilderDefault: 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:
Interaction with deferCollectionAllocation:
When both clearBuilder and deferCollectionAllocation are enabled:
clear() is a no-opRelated Issues:
See also: clear, deferCollectionAllocation, strictBuilder
copyOfDefault: "copyOf"
Naming template for static method that creates an immutable copy from an instance of the abstract value type or implementation.
Method behavior:
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:
See also: from, toImmutable, with, @Value.Immutable(copy = ...)
createDefault: "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 classesof for factory methodsWhen to use “new” vs factory method:
See also: typeModifiable, of, builder, @Value.Modifiable
defaultAsDefaultDefault: 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:
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
defaultsDefault: @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 instanceintern - Intern instancescopy - Generate copyOf and withX methodsprehash - Precompute hashCode in constructorlazyhash - Compute hashCode lazilybuilder - Generate builderSee also: @Value.Immutable, meta-annotations
deferCollectionAllocationDefault: 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:
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:
strictBuilder = true: Strict builders pre-allocate collectionsRelated Issues:
When to use:
See also: strictBuilder, clearBuilder
delegateToStringDefault: "" (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:
underrideToString - Custom toString in abstract classlimitStringLengthInToString - String length limitingUse 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:
this (the immutable instance)Generated code example:
@Override
public String toString() {
return com.example.ToStringUtils.stringify(this);
}
See also: underrideToString, limitStringLengthInToString, includeHashCode
depluralizeDefault: 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:
boats → boat, dogs → dogentries → entry, categories → categorydepluralizeDictionaryfeetPeople → feetPerson (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 Name | Mechanical Result | With Dictionary | Generated Method |
|---|---|---|---|
boats | boat | — | addBoat() |
people | people (no change) | person | addPerson() |
feet | feet (no change) | foot | addFoot() |
entries | entry (“ies”→“y”) | — | addEntry() |
categories | category | — | addCategory() |
feetPeople | feetPerson | foot:feet, person:people | addFeetPerson() |
peopleRepublics | peopleRepublic | person:people | addPeopleRepublic() |
data | data (no change) | — | addData() |
goods | goods (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” → feetPersonactiveUsers → last segment is “Users” → depluralized to “User” → activeUserAffected methods:
add: addPerson() instead of addPeople()addAll: addAllPeople() (plural preserved for “all”)put: For maps with pluralized keysExample (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:
addPerson() is more natural than addPeople()Dictionary-based depluralization: Handles both:
*s trimming (covers most cases)depluralizeDictionarySee also: depluralizeDictionary, add, addAll, put, @Value.Style.Depluralize
depluralizeDictionaryDefault: {} (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:
feetPeople, only “People” is checked against dictionaryDepluralization examples with dictionary:
| Attribute | Dictionary Entry | Mechanical | Final Result | Method Generated |
|---|---|---|---|---|
boats | — | boat | boat | addBoat() |
people | person:people | people | person | addPerson() |
children | child:children | children | child | addChild() |
feet | foot:feet | feet | foot | addFoot() |
mice | mouse:mice | mice | mouse | addMouse() |
data | data:data | data | data | addData() |
goods | goods | good | goods | addGoods() |
feetPeople | foot:feet, person:people | feetPeople | feetPerson | addFeetPerson() |
categories | — | category | category | addCategory() |
Short form syntax:
"goods" // Equivalent to "goods:goods"
Suppresses depluralization: goods → goods (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:
@Value.Style.Depluralize@Value.Style.Depluralize@Value.Style.DepluralizedepluralizeDictionary attributeExample (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:
See also: depluralize, @Value.Style.Depluralize, add
deepImmutablesDetectionDefault: 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:
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:
@Value.Derived or @Value.Default attributes (to avoid excessive complexity)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:
@Value.Parameter constructorList<Point> → addPoints(int x, int y))Shortcut initializers DON’T work for:
Note: As of version 2.2, the *Of suffix is no longer added to shortcut initializer 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 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:
Related Issues:
Example (value-fixture/test/org/immutables/fixture/deep/DeepImmutablesTest.java demonstrates usage)
Version notes:
*Of suffix (e.g., startOf(1, 2))start(1, 2))See also: attributeBuilderDetection, copyOf, builtinContainerAttributes
fallbackNullableAnnotationDefault: 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:
@Nullable String vs String @Nullable), they don’t always propagate automatically to generated signaturesDefault 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:
@Target settingsInherited.class is just a placeholder—it tells Immutables to look for javax.annotation.Nullable on classpathExample 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
finalInstanceFieldsDefault: 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:
protectedNoargConstructor, allows frameworks to create instances and then populate fieldsExample 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:
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:
copyOf methods: Most frameworks can work with immutable objects if you provide proper factory methodsWhat IS still final even with finalInstanceFields = false:
finalfinalWhat is affected:
Performance considerations:
Final fields can enable JVM optimizations:
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
forceEqualsInWithersDefault: 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:
== checks in some scenariosWhen 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:
with*() call producing a new instance for semanticsPerformance 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:
Trade-offs:
Pros:
Cons:
with*() callBest practices:
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 {}
Profile before enabling: Measure allocation rates to see if this optimization helps
Document behavior: Make it clear to API users that with*() may return same instance
forceJacksonIgnoreFieldsDefault: 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:
JsonSerializer/JsonDeserializerExample:
@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
forceJacksonPropertyNamesDefault: 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):
PropertyNamingStrategy to control namesExample 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
fromDefault: "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:
strictBuilder = true, from is automatically disabled (reinitializing values violates strict semantics)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:
| Feature | from | copyOf |
|---|---|---|
| Where | Builder method | Static factory method |
| Purpose | Initialize builder partially | Create complete immutable copy |
| Overrides | Can override values after | No modification after copy |
| Validation | Runs on build() | Runs immediately |
| Pattern | Builder pattern | Factory 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
generateSuppressAllWarningsDefault: 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:
When to disable (set to false):
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 Category | Suppressed with true | Visible with false |
|---|---|---|
| Unchecked | ✓ | ✗ |
| Deprecation | ✓ | ✗ |
| Raw types | ✓ | ✗ |
| Unused | ✓ | ✗ |
| Fallthrough | ✓ | ✗ |
| Serial | ✓ | ✗ |
| All others | ✓ | ✗ |
Best practices:
generateSuppressAllWarnings = true (default) in production codefalse when investigating generated code issues@SuppressWarnings("immutables") on abstract types when needed-Werror or failOnWarning, keep the default trueAlternative: 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
getDefault: "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"pre*fix" is NOT supportedEdge 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:
get = {"is*", "get*"} works well with Jackson, JPA, and other frameworksget = {"is*", "get*"} not get = {"get*", "is*"}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
getBuilderDefault: "*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:
attributeBuilderDetection is true@Value.Immutable typeExample 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:
@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
getBuildersDefault: "*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:
attributeBuilderDetection is true@Value.Immutable typeDifference from getBuilder:
@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:
// 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
headerCommentsDefault: 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:
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:
When to enable:
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:
See also: allowedClasspathAnnotations
immutableCopyOfRoutinesDefault: {} (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:
immutableCopyOf methodsBasic 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:
public static T immutableCopyOf(T value)Experimental status:
This feature is marked as experimental and may change in future releases:
Use with caution and be prepared for potential breaking changes in future Immutables versions.
When to use:
When NOT to use:
@Value.Check insteadAlternative: @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
implementationNestedInBuilderDefault: 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?
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:
| visibility | builderVisibility | implementationNestedInBuilder | Result |
|---|---|---|---|
| SAME | PUBLIC | false (default) | Standard: public immutable, nested public builder |
| PRIVATE | PUBLIC | true (automatic) | Public builder top-level, private immutable nested |
| PACKAGE | PUBLIC | false (default) | Package immutable top-level, public nested builder |
| PUBLIC | PRIVATE | false | Public immutable, private nested builder (builder hidden) |
Use cases:
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:
Cons:
See also: visibility, builderVisibility, overshadowImplementation
includeHashCodeDefault: "" (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?
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:
Use cases:
Best practice:
For most use cases, the default hash code is sufficient. Only use includeHashCode when:
See also: underrideHashCode, prehash, lazyhash
initDefault: "*"
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:
| Attribute | Used For | Default | Example |
|---|---|---|---|
| init | Builder initialization methods | "*" | builder.name("Alice") or builder.setName("Alice") |
| set | Modifiable “setter” methods | "set*" | modifiable.setName("Alice") |
| with | Immutable 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:
init = "set*" for frameworks expecting JavaBean patternsBest practices:
init = "set*" if your team uses set style"*" works well for most casesMeta-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;
instanceDefault: "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:
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 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
isInitializedDefault: "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:
@Nullable, Optional or @Value.Default or collections) are setExample: 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
isSetDefault: "*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:
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
isSetOnBuilderDefault: 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:
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:
When to leave disabled (default):
See also: isSet, canBuild, clearBuilder, strictBuilder
jacksonIntegrationType: boolean
Default: true
Controls whether special Jackson integration code is generated when @JsonSerialize/@JsonDeserialize annotations are detected on value types.
From org.immutables.value.Value.Style.jacksonIntegration():
Setting this to
falsewould 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 asJsonPropertyannotations or internalJsondelegate class together withJsonCreatormethod. This allows to placeJsonSerialize/JsonDeserializeannotations on the value types without redundant support code being generated.
When jacksonIntegration = true (default) and Jackson annotations are present:
@JsonProperty annotations on all attributes in the implementation classJson delegate class for deserialization@JsonCreator factory method for Jackson to call@JsonIgnore, @JsonInclude, etc.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
}
}
@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
}
}
Use jacksonIntegration = false when:
JsonDeserializer implementationsExample: 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();
}
}
@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")
}
Enabled (default):
Disabled:
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
jakartaType: 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.*.
From org.immutables.value.Value.Style.jakarta():
If enabled,
jakarta.*packages will take over any relevantjavax.*. This includes primarilyjakarta.annotation.*andjakarta.validation.*. Note that classpath inhibitor orallowedClasspathAnnotationswill still take effect, it’s just so that
With Java EE transitioning to Jakarta EE (under Eclipse Foundation), many packages were renamed from javax.* to jakarta.*:
javax.annotation.Generated → jakarta.annotation.Generatedjavax.annotation.Nullable → jakarta.annotation.Nullablejavax.validation.constraints.* → jakarta.validation.constraints.*This style attribute lets you control which namespace Immutables uses when generating code.
@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
}
@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
}
Enable jakarta = true when:
Keep jakarta = false when:
@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
}
}
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
Annotations affected by jakarta = true:
| javax namespace | jakarta namespace |
|---|---|
javax.annotation.Generated | jakarta.annotation.Generated |
javax.annotation.Nullable | jakarta.annotation.Nullable |
javax.validation.constraints.* | jakarta.validation.constraints.* |
javax.validation.Valid | jakarta.validation.Valid |
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();
}
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
Example (value-fixture/src/org/immutables/fixture/style/LostInJakarta.java:21):
@Value.Style(jakarta = true)
jakarta setting only affects auto-discovered annotations from classpathallowedClasspathAnnotations and classpath inhibitor still take effectSee also: allowedClasspathAnnotations, nullableAnnotation, fallbackNullableAnnotation
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.
From org.immutables.value.Value.Style.jdk9Collections():
When
true— will use JDK 9+ immutable collections to implementList/Set/Mapattributes. In JDK 9+, immutable collections are instantiated viaof/copyOfstatic methods onList,Set,Mapinterfaces:List.of(),Set.copyOf(Collection), etc. Please note that these collections do not supportnullelements, also Sets and Maps do not maintain insertion order, so the order is arbitrary and cannot be relied upon.
@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
}
@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()
);
}
}
}
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"]
Enable when:
Don’t enable when:
LinkedHashSet behaviorLinkedHashMap behavior| Feature | Guava (default) | JDK 9+ Collections |
|---|---|---|
| Null elements | ✗ Rejected | ✗ Rejected |
| List ordering | ✓ Maintained | ✓ Maintained |
| Set ordering | ✓ Insertion order | ✗ Arbitrary |
| Map ordering | ✓ Insertion order | ✗ Arbitrary |
| Dependency | Guava required | JDK 9+ only |
| Memory | Slightly higher | Slightly lower |
| Performance | Very fast | Very fast |
@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.
@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>
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"
Minimum JDK version:
List.of(), Set.of(), Map.of()List.copyOf(), Set.copyOf(), Map.copyOf()If using JDK 9 exactly, some methods may use of() instead of copyOf().
JDK 9+ collections are highly optimized:
Memory comparison (1000-element List):
ImmutableList: ~8KB + collection overheadList.copyOf(): ~8KB (slightly more compact)The difference is minimal but can add up in large applications.
See also: jdkOnly, builtinContainerAttributes
jdkOnlyType: boolean
Default: false
Forces generation of code using only JDK 7+ standard library classes, completely avoiding Google Guava dependencies.
From org.immutables.value.Value.Style.jdkOnly():
When
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 attributetrue— forces to generate code which use only JDK 7+ standard library classes. It isfalseby 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.
@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
}
@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
}
Enable jdkOnly = true when:
Keep jdkOnly = false when:
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.
Collections:
| Feature | Guava | JDK-only |
|---|---|---|
| Lists | ImmutableList | Defensive copy + Collections.unmodifiableList() |
| Sets | ImmutableSet | Defensive copy + Collections.unmodifiableSet() |
| Maps | ImmutableMap | Defensive copy + Collections.unmodifiableMap() |
| Performance | Slightly faster | Slightly slower (defensive copying) |
| Memory | Lower overhead | Higher overhead (double storage) |
Optional:
com.google.common.base.Optionaljava.util.Optional (JDK 8+)@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
@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:
| Configuration | Collection Implementation |
|---|---|
jdkOnly = false | Guava ImmutableList |
jdkOnly = true, jdk9Collections = false | JDK 7 Collections.unmodifiableList() |
jdkOnly = true, jdk9Collections = true | JDK 9+ List.copyOf() |
Guava collections (jdkOnly = false):
JDK Collections.unmodifiable (jdkOnly = true, jdk9Collections = false):*
JDK 9+ copyOf (jdkOnly = true, jdk9Collections = true):
Benchmark (1000-element List, 1 million iterations):
The difference is negligible for most applications.
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>
@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:
jdkOnly: +2.3 MB (Guava library)jdkOnly: +50 KB (only Immutables runtime)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
legacyAccessorOrderingType: 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.
From org.immutables.value.Value.Style.legacyAccessorOrdering():
When set to
true, processor will switch to legacy accessor (attribute) source ordering traversal method. Prior to Immutablesv2.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 inv2.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-Done)org.immutables.useLegacyAccessorOrdering=true
In Immutables v2.7.5, the attribute ordering logic changed to fix inheritance issues with interfaces. This affected:
toString() output orderhashCode() calculation orderOrdering: 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
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
Use legacyAccessorOrdering = true when:
toString() outputUse default (false) when:
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.
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>
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!
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"}
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.
@Value.Style(legacyAccessorOrdering = true)
package com.example.legacy;
// All immutables in this package use legacy ordering
See also: passAnnotations, additionalJsonAnnotations
limitStringLengthInToStringType: 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.
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:
@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
@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
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
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
@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)
@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()
}
}
Very minimal:
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();
}
Typical values:
Don’t set too low: Values below 20 may make debugging harder by hiding too much information.
See also: delegateToString, underrideToString
mergeFromSupertypesDynamicallyType: 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.
From org.immutables.value.Value.Style.mergeFromSupertypesDynamically():
Builder has
frommethod 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 runtimeinstanceofcheck 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 inClassCastExceptionor 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
mergeFromSupertypesDynamicallystyle flag is set tofalse, the generated code will switch to using simpler copy logic in compile-time resolved overloads. The default istrueto useinstanceofchecks and bit masks under the hood to extract all attributes using all implemented supertypes.
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();
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
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();
Dynamic (default):
instanceof checksStatic (mergeFromSupertypesDynamically = false):
For most use cases, the performance difference is negligible.
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();
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) { ... }
Use default (true) when:
Use false when:
See also: from, allParameters
newBuilderType: 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.
From org.immutables.value.Value.Style.newBuilder():
Builder creator method, it differs from
builderin 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.
builder and newBuilderbuilder: Naming for builder accessor from the immutable class (e.g., ImmutablePerson.builder())newBuilder: Naming for standalone top-level buildersFor most use cases, both point to the same builder, but they control different access points.
@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
@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();
implementationNestedInBuilderWhen 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();
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'
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();
Use custom naming when:
create, newInstance, makeimplementationNestedInBuilderKeep default (“new”) when:
new are familiar@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
nullableAnnotationType: 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.
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
nullableAnnotationcan 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.Nullableandjavax.annotation.CheckForNullare always recognized as nullable annotations.
The following annotations are always recognized as nullable, regardless of nullableAnnotation setting:
javax.annotation.Nullablejavax.annotation.CheckForNulljakarta.annotation.Nullable (when jakarta = true)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();
@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();
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
}
)
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);
}
Different frameworks/libraries have their own nullable annotations:
| Framework | Package | Annotation |
|---|---|---|
| IntelliJ IDEA | org.jetbrains.annotations | @Nullable |
| Android | androidx.annotation | @Nullable |
| Eclipse | org.eclipse.jdt.annotation | @Nullable |
| JSR-305 | javax.annotation | @Nullable |
| Jakarta | jakarta.annotation | @Nullable |
| FindBugs | edu.umd.cs.findbugs.annotations | @Nullable |
| SpotBugs | edu.umd.cs.spotbugs.annotations | @Nullable |
| Checker Framework | org.checkerframework.checker.nullness.qual | @Nullable |
All of these work by default because they share the simple name "Nullable".
Customize when:
@Optional or @CanBeEmptyExample: 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();
}
@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();
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
ofType: 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.
From org.immutables.value.Value.Style.of():
Constructor method name.
Since version
2.1.5you 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 ifCheckorImmutable#singleton()is used and certain other functionality. In such cases compile error would be raised.
of is usedThe of() naming is only applicable when you have constructor parameters. This happens when:
allParameters = true: All attributes become constructor parametersallMandatoryParameters = true: Required attributes become constructor parameters@Value.Parameter annotations: Individual attributes marked as constructor parameters@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);
@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);
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);
Cannot use of = "new" with:
@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
}
}
@Value.Style(of = "new") // Compilation ERROR
@Value.Immutable(singleton = true)
interface Config {
@Value.Default
default String name() {
return "default";
}
}
@Value.Immutable(builder = false) mode| Feature | Factory method (of = "of") | Constructor (of = "new") |
|---|---|---|
| Validation | Can run @Value.Check | Cannot run @Value.Check |
| Normalization | Can transform inputs | Direct assignment only |
| Return type | Can return interface | Must return concrete class |
| Singleton support | Works | Doesn’t work |
| Flexibility | High | Low |
| Familiarity | Less common in Java | Standard Java pattern |
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());
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
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();
@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*")
Use of = "of" (default) when:
Use of = "new" when:
Use custom names when:
rgb(), point(), token())See also: allParameters, allMandatoryParameters, instance, newBuilder
optionalAcceptNullableType: 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.
From org.immutables.value.Value.Style.optionalAcceptNullable():
Specify whether init, copy and factory methods and constructors for an unwrapped
XofOptional<X>should acceptnullvalues as empty value. By default, nulls are rejected in favor of explicit conversion usingOptional.ofNullable. Please note that initializers that take explicitOptionalvalue always reject nulls regardless of this setting.
When you have an Optional<T> attribute, Immutables generates two setter methods on the builder:
T directly (e.g., value(String))Optional<T> (e.g., value(Optional<String>))This attribute controls whether the unwrapped setter accepts null.
@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
@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
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
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();
Enable optionalAcceptNullable = true when:
Keep default (false) when:
Optional.empty() for readabilityWithout 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;
}
@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();
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!
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:
See also: nullableAnnotation, defaultAsDefault
overshadowImplementationType: 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.
From org.immutables.value.Value.Style.overshadowImplementation():
Makes abstract value type predominantly used in generated signatures rather than immutable implementation class. In case of
Note: not all generators or generation modes might honor this attributevisibilityis more restrictive thanbuilderVisibility(for example isPRIVATE), then this feature is turned on automatically.
@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();
@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
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
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
@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
@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();
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();
@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
)
Use overshadowImplementation = true when:
Use default (false) when:
Minimal overhead:
See also: visibility, implementationNestedInBuilder, builderVisibility
packageGeneratedType: 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.
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.
| Pattern | Example | Result |
|---|---|---|
"*" (default) | com.example.model → com.example.model | Same package |
"*.generated" | com.example.model → com.example.model.generated | Subpackage |
"*.impl" | com.example.api → com.example.api.impl | Implementation subpackage |
"com.myapp.gen" | Any package → com.myapp.gen | Fixed package |
"*.immutables.impl" | com.api → com.api.immutables.impl | Nested subpackage |
package com.example.model;
@Value.Immutable
interface User {
String name();
}
// Generated class in SAME package:
// com.example.model.ImmutableUser
@Value.Style(packageGenerated = "*.generated")
package com.example.model;
@Value.Immutable
interface User {
String name();
}
// Generated class in subpackage:
// com.example.model.generated.ImmutableUser
@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
Same package (default):
Different package:
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
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>
When using subpackages for generated code:
# Ignore all generated subpackages
**/generated/
# Or specific pattern:
**/model/generated/
**/api/impl/
// 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
DO:
.generated or .impl suffixesDON’T:
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
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>
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
passAnnotationsType: 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.
From org.immutables.value.Value.Style.passAnnotations():
This has a number of limitations, including that it’s not suited for
ElementType.TYPE_USEannotations (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 usingfallbackNullableAnnotationso it will be used in generated code in all places where original annotations would not propagate automatically.
Already handled automatically:
@Nullable annotations (all variants)@javax.annotation.CheckForNull@JsonProperty, @JsonIgnore, etc.)@Inherited annotations (Java’s built-in inheritance)Requires 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;
}
@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;
}
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;
}
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();
}
@Value.Style(passAnnotations = {
ValidEmail.class,
NonNegative.class
})
@Value.Immutable
interface RegistrationForm {
@ValidEmail
String email();
@NonNegative
int age();
}
// Validation framework sees annotations on implementation
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
// @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();
}
// At runtime:
boolean hasAnnotation = ImmutableUser.class.isAnnotationPresent(MyAnnotation.class);
// On field:
Field field = ImmutableUser.class.getDeclaredField("name");
boolean hasFieldAnnotation = field.isAnnotationPresent(FieldAnnotation.class);
DO:
DON’T:
@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
privateNoargConstructorType: 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.
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()returnstrue.
@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;
}
}
@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;
}
}
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();
@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()
@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
}
@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
Security concerns:
Immutability concerns:
@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
protectedNoargConstructorDefault: 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
putDefault: "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
putAllDefault: "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
redactedMaskDefault: "" (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
setDefault: "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
setBuilderDefault: "*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
setJacksonPropertyRequiredDefault: true
Generates @JsonProperty(required=true) for mandatory attributes.
@Value.Style(setJacksonPropertyRequired = false)
See also: jacksonIntegration
stagedBuilderDefault: 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:
Note: May not play well with other functionality. May be auto-disabled in certain configurations like implementations nested in builder.
See also: strictBuilder
strictBuilderDefault: false
Generates forward-only strict builders. Prevents reinitialization errors:
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:
With* interface generationSee also: stagedBuilder, from
strictModifiableType: boolean
Default: true
Controls whether modifiable companion class accessors throw exceptions when accessing uninitialized mandatory attributes.
Strict modifiable will refuse any accessor value (by throwing
IllegalStateException) which is mandatory. Enabled by default. Set it tofalseand it will allow to get current field value even if not initialized (nullfor references,0,false— for primitives).
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.
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:
null0 (byte, short, int, long, float, double)false'\0'Use strict mode (true) when:
Use lenient mode (false) when:
@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
Does NOT affect:
strictBuildertoImmutable() still validates mandatory attributesWorks with:
typeModifiable - Naming of modifiable classtoImmutable - Converting modifiable to immutablevalidationMethod - Validation when converting to immutableMistake 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.
toImmutable()Optional<T> for truly optional fields instead of lenient mode@Value.Modifiable)IllegalStateException in strict mode)See also: typeModifiable, strictBuilder, toImmutable, @Value.Modifiable
throwForInvalidImmutableStateType: 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.
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.
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]
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);
}
Mandatory constructor: public ExceptionType(String message)
Optional constructor: public ExceptionType(String... missingParams) or public ExceptionType(String[] missingParams)
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));
}
}
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
)
Builder context:
build() with missing mandatory attributesbuild() when @Value.Check validation failsFactory method context:
of() factory methods (with @Value.Parameter)Modifiable context:
toImmutable() on modifiable with missing mandatory attributesWorks with:
throwForNullPointer - Separate exception for null violationsvalidationMethod - Controls what validation is performedstrictBuilder - May throw earlier when attributes set multiple times@Value.Check - Custom validation throwing same exception typeDoes NOT affect:
@Value.Default, @Value.Derived, @Value.Check methodsMistake 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
}
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.
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
}
}
@Value.Check, @Value.Default, or @Value.Derived methodsSee also: throwForNullPointer, validationMethod, strictBuilder, @Value.Check, buildOrThrow
throwForNullPointerType: 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.
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
NullPointerExceptionand the calls are usually delegated toObjects.requireNonNull(Object)or similar utility throwingNullPointerException.
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.
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);
}
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();
Null checks are NOT performed for:
null converted to Optional.empty()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();
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)
Works with:
throwForInvalidImmutableState - Separate exception for missing attributesvalidationMethod - Controls overall validation behaviornullableAnnotation - Recognizes which attributes allow null@Nullable - Attributes marked nullable bypass null checksDoes NOT affect:
@Value.Check, @Value.Derived, @Value.Default)addAll(null) or putAll(null)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.
// 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
}
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);
}
}
Objects.requireNonNull() pattern)@Value.Check, @Value.Derived, etc.)@Nullable attributes, Optional attributes, or default attributesSee also: throwForInvalidImmutableState, validationMethod, nullableAnnotation, @Nullable
toBuilderDefault: "" (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:
strictBuilderBuilder toBuilder() in abstract value type is not considered an attributeSee also: from
toImmutableDefault: "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
transientDerivedFieldsDefault: 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
typeAbstractDefault: "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
typeBuilderDefault: "Builder"
Generated builder class name template.
@Value.Style(typeBuilder = "Maker")
@Value.Immutable
interface Value {
String data();
}
// Generated: ImmutableValue.Maker
See also: builder, typeInnerBuilder
typeImmutableDefault: "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
typeImmutableEnclosingDefault: "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
typeImmutableNestedDefault: "*"
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
typeInnerBuilderDefault: "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
typeInnerModifiableDefault: "Modifiable"
Inner modifiable class name for matching existing modifiable to extend.
@Value.Style(typeInnerModifiable = "Mutable")
See also: typeModifiable
typeModifiableDefault: "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
typeWithDefault: "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
underrideEqualsDefault: "" (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
underrideHashCodeDefault: "" (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
underrideToStringDefault: "" (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
unsafeDefaultAndDerivedDefault: 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
unsetDefault: "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
validationMethodDefault: 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.
visibilityDefault: ImplementationVisibility.SAME
Controls visibility of generated immutable implementation class.
Options:
PUBLIC - Always publicSAME - Same as abstract value typeSAME_NON_RETURNED - Same visibility but not returned from build/factory (deprecated, use with overshadowImplementation)PACKAGE - Package-privatePRIVATE - 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
visibilityStringDefault: "" (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
weakInterningDefault: 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)
withDefault: "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
withUnaryOperatorDefault: "" (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