0
# Validation and Constraints
1
2
Built-in validation framework with constraint checking, invariant validation, custom exception handling, and attribute privacy controls for ensuring object consistency and correctness.
3
4
## Capabilities
5
6
### Invariant Validation
7
8
Mark methods for automatic invariant validation after instance creation.
9
10
```java { .api }
11
/**
12
* Annotates method that should be invoked internally to validate invariants
13
* after instance has been created, but before returned to client.
14
* Method must be parameter-less, non-private, void return type, and not
15
* throw checked exceptions.
16
*/
17
@interface Value.Check { }
18
```
19
20
**Usage Examples:**
21
22
```java
23
@Value.Immutable
24
public interface Rectangle {
25
double width();
26
double height();
27
28
@Value.Check
29
protected void validate() {
30
if (width() <= 0) {
31
throw new IllegalStateException("Width must be positive, got: " + width());
32
}
33
if (height() <= 0) {
34
throw new IllegalStateException("Height must be positive, got: " + height());
35
}
36
}
37
}
38
39
// Validation runs automatically during construction
40
Rectangle rect = ImmutableRectangle.builder()
41
.width(10.0)
42
.height(5.0)
43
.build(); // Validation passes
44
45
// This would throw IllegalStateException during build()
46
// Rectangle invalid = ImmutableRectangle.builder()
47
// .width(-5.0)
48
// .height(10.0)
49
// .build();
50
51
// Normalization with @Value.Check returning instance
52
@Value.Immutable
53
public interface NormalizedText {
54
String text();
55
56
@Value.Check
57
default NormalizedText normalize() {
58
String normalized = text().trim().toLowerCase();
59
if (!normalized.equals(text())) {
60
return ImmutableNormalizedText.builder()
61
.text(normalized)
62
.build();
63
}
64
return this; // Return this if already normalized
65
}
66
}
67
68
// Automatic normalization during construction
69
NormalizedText text = ImmutableNormalizedText.builder()
70
.text(" Hello World ")
71
.build();
72
73
assert text.text().equals("hello world"); // Normalized automatically
74
```
75
76
### Attribute Privacy and Security
77
78
Control attribute visibility and secure sensitive data in string representations.
79
80
```java { .api }
81
/**
82
* Marks attribute for exclusion from auto-generated toString() method.
83
* Can be excluded completely or replaced with masking characters.
84
*/
85
@interface Value.Redacted { }
86
```
87
88
**Usage Examples:**
89
90
```java
91
@Value.Immutable
92
public interface UserCredentials {
93
String username();
94
95
@Value.Redacted
96
String password();
97
98
@Value.Redacted
99
String apiKey();
100
101
String email();
102
}
103
104
UserCredentials creds = ImmutableUserCredentials.builder()
105
.username("alice")
106
.password("secret123")
107
.apiKey("sk-1234567890abcdef")
108
.email("alice@example.com")
109
.build();
110
111
// Redacted attributes hidden from toString()
112
System.out.println(creds);
113
// Output: UserCredentials{username=alice, email=alice@example.com}
114
// password and apiKey are omitted
115
116
// Custom masking with Style configuration
117
@Value.Style(redactedMask = "***")
118
@Value.Immutable
119
public interface MaskedCredentials {
120
String username();
121
122
@Value.Redacted
123
String password();
124
}
125
126
MaskedCredentials masked = ImmutableMaskedCredentials.builder()
127
.username("bob")
128
.password("topsecret")
129
.build();
130
131
System.out.println(masked);
132
// Output: MaskedCredentials{username=bob, password=***}
133
```
134
135
### Custom Exception Handling
136
137
Configure custom exception types for validation failures.
138
139
```java { .api }
140
/**
141
* Style configuration for custom exception types
142
*/
143
@interface Value.Style {
144
/**
145
* Runtime exception to throw when immutable object is in invalid state.
146
* Exception class must have constructor taking single string.
147
*/
148
Class<? extends RuntimeException> throwForInvalidImmutableState()
149
default IllegalStateException.class;
150
151
/**
152
* Runtime exception to throw when null reference is passed to
153
* non-nullable parameter. Default is NullPointerException.
154
*/
155
Class<? extends RuntimeException> throwForNullPointer()
156
default NullPointerException.class;
157
}
158
```
159
160
**Usage Examples:**
161
162
```java
163
// Custom validation exception
164
public class ValidationException extends RuntimeException {
165
public ValidationException(String message) {
166
super(message);
167
}
168
169
public ValidationException(String... missingFields) {
170
super("Missing required fields: " + String.join(", ", missingFields));
171
}
172
}
173
174
@Value.Style(
175
throwForInvalidImmutableState = ValidationException.class,
176
throwForNullPointer = ValidationException.class
177
)
178
@Value.Immutable
179
public interface ValidatedOrder {
180
@Nullable String customerId();
181
List<String> items();
182
BigDecimal total();
183
184
@Value.Check
185
protected void validateOrder() {
186
if (items().isEmpty()) {
187
throw new ValidationException("Order must contain at least one item");
188
}
189
if (total().compareTo(BigDecimal.ZERO) < 0) {
190
throw new ValidationException("Order total cannot be negative");
191
}
192
}
193
}
194
195
// Custom exceptions thrown on validation failures
196
try {
197
ValidatedOrder order = ImmutableValidatedOrder.builder()
198
.customerId("123")
199
.total(new BigDecimal("100.00"))
200
// Missing items
201
.build();
202
} catch (ValidationException e) {
203
// Custom exception with meaningful message
204
System.err.println(e.getMessage()); // "Missing required fields: items"
205
}
206
```
207
208
### Bean Validation Integration
209
210
Integration with JSR 303 Bean Validation API for declarative constraint validation.
211
212
```java { .api }
213
/**
214
* Bean Validation integration via ValidationMethod.VALIDATION_API
215
* Disables null checks in favor of @NotNull annotations and uses
216
* static validator per object type.
217
*/
218
enum ValidationMethod {
219
VALIDATION_API // Use JSR 303 Bean Validation API
220
}
221
```
222
223
**Usage Examples:**
224
225
```java
226
import javax.validation.constraints.*;
227
228
@Value.Style(validationMethod = ValidationMethod.VALIDATION_API)
229
@Value.Immutable
230
public interface ValidatedUser {
231
@NotNull
232
@Size(min = 2, max = 50)
233
String firstName();
234
235
@NotNull
236
@Size(min = 2, max = 50)
237
String lastName();
238
239
@NotNull
240
241
String email();
242
243
@Min(0)
244
@Max(120)
245
Integer age();
246
247
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$")
248
String phoneNumber();
249
250
@DecimalMin("0.0")
251
@Digits(integer = 10, fraction = 2)
252
BigDecimal salary();
253
}
254
255
// Bean Validation constraints enforced during construction
256
ValidatedUser user = ImmutableValidatedUser.builder()
257
.firstName("John")
258
.lastName("Doe")
259
.email("john.doe@example.com")
260
.age(30)
261
.phoneNumber("+1234567890")
262
.salary(new BigDecimal("50000.00"))
263
.build(); // All constraints validated
264
265
// Constraint violations throw ConstraintViolationException
266
try {
267
ValidatedUser invalid = ImmutableValidatedUser.builder()
268
.firstName("J") // Too short
269
.lastName("Doe")
270
.email("invalid-email") // Invalid format
271
.age(150) // Too old
272
.phoneNumber("invalid") // Invalid pattern
273
.salary(new BigDecimal("-1000")) // Below minimum
274
.build();
275
} catch (ConstraintViolationException e) {
276
e.getConstraintViolations().forEach(violation ->
277
System.err.println(violation.getPropertyPath() + ": " + violation.getMessage())
278
);
279
}
280
```
281
282
### Complex Validation Scenarios
283
284
Advanced validation patterns for complex business rules and cross-field validation.
285
286
**Usage Examples:**
287
288
```java
289
@Value.Immutable
290
public interface DateRange {
291
LocalDate startDate();
292
LocalDate endDate();
293
294
@Value.Check
295
protected void validateRange() {
296
if (startDate().isAfter(endDate())) {
297
throw new IllegalStateException(
298
"Start date " + startDate() + " must be before end date " + endDate()
299
);
300
}
301
302
long daysBetween = ChronoUnit.DAYS.between(startDate(), endDate());
303
if (daysBetween > 365) {
304
throw new IllegalStateException(
305
"Date range cannot exceed 365 days, got " + daysBetween + " days"
306
);
307
}
308
}
309
310
@Value.Derived
311
default long durationDays() {
312
return ChronoUnit.DAYS.between(startDate(), endDate()) + 1;
313
}
314
}
315
316
// Complex validation with business rules
317
@Value.Immutable
318
public interface Loan {
319
BigDecimal principal();
320
BigDecimal interestRate();
321
int termMonths();
322
String borrowerCreditScore();
323
324
@Value.Check
325
protected void validateLoan() {
326
// Principal limits
327
if (principal().compareTo(new BigDecimal("1000")) < 0) {
328
throw new IllegalStateException("Minimum loan amount is $1,000");
329
}
330
if (principal().compareTo(new BigDecimal("1000000")) > 0) {
331
throw new IllegalStateException("Maximum loan amount is $1,000,000");
332
}
333
334
// Interest rate validation
335
if (interestRate().compareTo(BigDecimal.ZERO) <= 0 ||
336
interestRate().compareTo(new BigDecimal("50")) > 0) {
337
throw new IllegalStateException("Interest rate must be between 0% and 50%");
338
}
339
340
// Term validation
341
if (termMonths < 1 || termMonths > 360) {
342
throw new IllegalStateException("Loan term must be between 1 and 360 months");
343
}
344
345
// Credit score dependent validation
346
int creditScore = Integer.parseInt(borrowerCreditScore());
347
if (creditScore < 300 || creditScore > 850) {
348
throw new IllegalStateException("Credit score must be between 300 and 850");
349
}
350
351
// Business rule: high principal requires good credit
352
if (principal().compareTo(new BigDecimal("100000")) > 0 && creditScore < 700) {
353
throw new IllegalStateException(
354
"Loans over $100,000 require credit score of at least 700"
355
);
356
}
357
}
358
359
@Value.Lazy
360
default BigDecimal monthlyPayment() {
361
// Complex calculation using all validated fields
362
double monthlyRate = interestRate().doubleValue() / 100.0 / 12.0;
363
double amount = principal().doubleValue();
364
365
if (monthlyRate == 0) {
366
return principal().divide(BigDecimal.valueOf(termMonths), 2, RoundingMode.HALF_UP);
367
}
368
369
double payment = amount * (monthlyRate * Math.pow(1 + monthlyRate, termMonths))
370
/ (Math.pow(1 + monthlyRate, termMonths) - 1);
371
372
return BigDecimal.valueOf(payment).setScale(2, RoundingMode.HALF_UP);
373
}
374
}
375
```