Compile-time annotation processor for generating immutable value objects with builder patterns and compile-time validation.
—
Built-in validation framework with constraint checking, invariant validation, custom exception handling, and attribute privacy controls for ensuring object consistency and correctness.
Mark methods for automatic invariant validation after instance creation.
/**
* Annotates method that should be invoked internally to validate invariants
* after instance has been created, but before returned to client.
* Method must be parameter-less, non-private, void return type, and not
* throw checked exceptions.
*/
@interface Value.Check { }Usage Examples:
@Value.Immutable
public interface Rectangle {
double width();
double height();
@Value.Check
protected void validate() {
if (width() <= 0) {
throw new IllegalStateException("Width must be positive, got: " + width());
}
if (height() <= 0) {
throw new IllegalStateException("Height must be positive, got: " + height());
}
}
}
// Validation runs automatically during construction
Rectangle rect = ImmutableRectangle.builder()
.width(10.0)
.height(5.0)
.build(); // Validation passes
// This would throw IllegalStateException during build()
// Rectangle invalid = ImmutableRectangle.builder()
// .width(-5.0)
// .height(10.0)
// .build();
// Normalization with @Value.Check returning instance
@Value.Immutable
public interface NormalizedText {
String text();
@Value.Check
default NormalizedText normalize() {
String normalized = text().trim().toLowerCase();
if (!normalized.equals(text())) {
return ImmutableNormalizedText.builder()
.text(normalized)
.build();
}
return this; // Return this if already normalized
}
}
// Automatic normalization during construction
NormalizedText text = ImmutableNormalizedText.builder()
.text(" Hello World ")
.build();
assert text.text().equals("hello world"); // Normalized automaticallyControl attribute visibility and secure sensitive data in string representations.
/**
* Marks attribute for exclusion from auto-generated toString() method.
* Can be excluded completely or replaced with masking characters.
*/
@interface Value.Redacted { }Usage Examples:
@Value.Immutable
public interface UserCredentials {
String username();
@Value.Redacted
String password();
@Value.Redacted
String apiKey();
String email();
}
UserCredentials creds = ImmutableUserCredentials.builder()
.username("alice")
.password("secret123")
.apiKey("sk-1234567890abcdef")
.email("alice@example.com")
.build();
// Redacted attributes hidden from toString()
System.out.println(creds);
// Output: UserCredentials{username=alice, email=alice@example.com}
// password and apiKey are omitted
// Custom masking with Style configuration
@Value.Style(redactedMask = "***")
@Value.Immutable
public interface MaskedCredentials {
String username();
@Value.Redacted
String password();
}
MaskedCredentials masked = ImmutableMaskedCredentials.builder()
.username("bob")
.password("topsecret")
.build();
System.out.println(masked);
// Output: MaskedCredentials{username=bob, password=***}Configure custom exception types for validation failures.
/**
* Style configuration for custom exception types
*/
@interface Value.Style {
/**
* Runtime exception to throw when immutable object is in invalid state.
* Exception class must have constructor taking single string.
*/
Class<? extends RuntimeException> throwForInvalidImmutableState()
default IllegalStateException.class;
/**
* Runtime exception to throw when null reference is passed to
* non-nullable parameter. Default is NullPointerException.
*/
Class<? extends RuntimeException> throwForNullPointer()
default NullPointerException.class;
}Usage Examples:
// Custom validation exception
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
public ValidationException(String... missingFields) {
super("Missing required fields: " + String.join(", ", missingFields));
}
}
@Value.Style(
throwForInvalidImmutableState = ValidationException.class,
throwForNullPointer = ValidationException.class
)
@Value.Immutable
public interface ValidatedOrder {
@Nullable String customerId();
List<String> items();
BigDecimal total();
@Value.Check
protected void validateOrder() {
if (items().isEmpty()) {
throw new ValidationException("Order must contain at least one item");
}
if (total().compareTo(BigDecimal.ZERO) < 0) {
throw new ValidationException("Order total cannot be negative");
}
}
}
// Custom exceptions thrown on validation failures
try {
ValidatedOrder order = ImmutableValidatedOrder.builder()
.customerId("123")
.total(new BigDecimal("100.00"))
// Missing items
.build();
} catch (ValidationException e) {
// Custom exception with meaningful message
System.err.println(e.getMessage()); // "Missing required fields: items"
}Integration with JSR 303 Bean Validation API for declarative constraint validation.
/**
* Bean Validation integration via ValidationMethod.VALIDATION_API
* Disables null checks in favor of @NotNull annotations and uses
* static validator per object type.
*/
enum ValidationMethod {
VALIDATION_API // Use JSR 303 Bean Validation API
}Usage Examples:
import javax.validation.constraints.*;
@Value.Style(validationMethod = ValidationMethod.VALIDATION_API)
@Value.Immutable
public interface ValidatedUser {
@NotNull
@Size(min = 2, max = 50)
String firstName();
@NotNull
@Size(min = 2, max = 50)
String lastName();
@NotNull
@Email
String email();
@Min(0)
@Max(120)
Integer age();
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$")
String phoneNumber();
@DecimalMin("0.0")
@Digits(integer = 10, fraction = 2)
BigDecimal salary();
}
// Bean Validation constraints enforced during construction
ValidatedUser user = ImmutableValidatedUser.builder()
.firstName("John")
.lastName("Doe")
.email("john.doe@example.com")
.age(30)
.phoneNumber("+1234567890")
.salary(new BigDecimal("50000.00"))
.build(); // All constraints validated
// Constraint violations throw ConstraintViolationException
try {
ValidatedUser invalid = ImmutableValidatedUser.builder()
.firstName("J") // Too short
.lastName("Doe")
.email("invalid-email") // Invalid format
.age(150) // Too old
.phoneNumber("invalid") // Invalid pattern
.salary(new BigDecimal("-1000")) // Below minimum
.build();
} catch (ConstraintViolationException e) {
e.getConstraintViolations().forEach(violation ->
System.err.println(violation.getPropertyPath() + ": " + violation.getMessage())
);
}Advanced validation patterns for complex business rules and cross-field validation.
Usage Examples:
@Value.Immutable
public interface DateRange {
LocalDate startDate();
LocalDate endDate();
@Value.Check
protected void validateRange() {
if (startDate().isAfter(endDate())) {
throw new IllegalStateException(
"Start date " + startDate() + " must be before end date " + endDate()
);
}
long daysBetween = ChronoUnit.DAYS.between(startDate(), endDate());
if (daysBetween > 365) {
throw new IllegalStateException(
"Date range cannot exceed 365 days, got " + daysBetween + " days"
);
}
}
@Value.Derived
default long durationDays() {
return ChronoUnit.DAYS.between(startDate(), endDate()) + 1;
}
}
// Complex validation with business rules
@Value.Immutable
public interface Loan {
BigDecimal principal();
BigDecimal interestRate();
int termMonths();
String borrowerCreditScore();
@Value.Check
protected void validateLoan() {
// Principal limits
if (principal().compareTo(new BigDecimal("1000")) < 0) {
throw new IllegalStateException("Minimum loan amount is $1,000");
}
if (principal().compareTo(new BigDecimal("1000000")) > 0) {
throw new IllegalStateException("Maximum loan amount is $1,000,000");
}
// Interest rate validation
if (interestRate().compareTo(BigDecimal.ZERO) <= 0 ||
interestRate().compareTo(new BigDecimal("50")) > 0) {
throw new IllegalStateException("Interest rate must be between 0% and 50%");
}
// Term validation
if (termMonths < 1 || termMonths > 360) {
throw new IllegalStateException("Loan term must be between 1 and 360 months");
}
// Credit score dependent validation
int creditScore = Integer.parseInt(borrowerCreditScore());
if (creditScore < 300 || creditScore > 850) {
throw new IllegalStateException("Credit score must be between 300 and 850");
}
// Business rule: high principal requires good credit
if (principal().compareTo(new BigDecimal("100000")) > 0 && creditScore < 700) {
throw new IllegalStateException(
"Loans over $100,000 require credit score of at least 700"
);
}
}
@Value.Lazy
default BigDecimal monthlyPayment() {
// Complex calculation using all validated fields
double monthlyRate = interestRate().doubleValue() / 100.0 / 12.0;
double amount = principal().doubleValue();
if (monthlyRate == 0) {
return principal().divide(BigDecimal.valueOf(termMonths), 2, RoundingMode.HALF_UP);
}
double payment = amount * (monthlyRate * Math.pow(1 + monthlyRate, termMonths))
/ (Math.pow(1 + monthlyRate, termMonths) - 1);
return BigDecimal.valueOf(payment).setScale(2, RoundingMode.HALF_UP);
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-immutables--value