0
# Custom Constraints
1
2
Framework for defining custom validation constraints through annotations and validator implementations with support for composed constraints and cascading validation.
3
4
## Capabilities
5
6
### Constraint Definition
7
8
Meta-annotation for marking annotations as Jakarta Validation constraints.
9
10
```java { .api }
11
/**
12
* Marks an annotation as being a Jakarta Validation constraint
13
* Must be applied to constraint annotations
14
*/
15
@Target({ANNOTATION_TYPE})
16
@Retention(RUNTIME)
17
@interface Constraint {
18
/**
19
* ConstraintValidator classes must reference distinct target types
20
* If target types overlap, a UnexpectedTypeException is raised
21
* @return array of ConstraintValidator classes implementing validation logic
22
*/
23
Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
24
}
25
```
26
27
### Constraint Validator
28
29
Interface defining the validation logic for custom constraints.
30
31
```java { .api }
32
/**
33
* Defines the logic to validate a given constraint for a given object type
34
* Implementations must be thread-safe
35
* @param <A> the annotation type handled by this validator
36
* @param <T> the target type supported by this validator
37
*/
38
interface ConstraintValidator<A extends Annotation, T> {
39
/**
40
* Initialize the validator in preparation for isValid calls
41
* @param constraintAnnotation annotation instance for a given constraint declaration
42
*/
43
default void initialize(A constraintAnnotation) {
44
// Default implementation does nothing
45
}
46
47
/**
48
* Implement the validation logic
49
* @param value object to validate (can be null)
50
* @param context context in which the constraint is evaluated
51
* @return false if value does not pass the constraint
52
*/
53
boolean isValid(T value, ConstraintValidatorContext context);
54
}
55
```
56
57
### Constraint Validator Context
58
59
Context providing contextual data and operation when applying a constraint validator.
60
61
```java { .api }
62
/**
63
* Provides contextual data and operations when applying a ConstraintValidator
64
*/
65
interface ConstraintValidatorContext {
66
/**
67
* Disable the default constraint violation
68
* Useful when building custom constraint violations
69
*/
70
void disableDefaultConstraintViolation();
71
72
/**
73
* Get the default constraint message template
74
* @return default message template
75
*/
76
String getDefaultConstraintMessageTemplate();
77
78
/**
79
* Get the ClockProvider for time-based validations
80
* @return ClockProvider instance
81
*/
82
ClockProvider getClockProvider();
83
84
/**
85
* Build a constraint violation with custom message template
86
* @param messageTemplate message template for the violation
87
* @return ConstraintViolationBuilder for building custom violations
88
*/
89
ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate);
90
91
/**
92
* Unwrap the context to a specific type
93
* @param type target type to unwrap to
94
* @return unwrapped instance
95
* @throws ValidationException if unwrapping is not supported
96
*/
97
<T> T unwrap(Class<T> type);
98
99
/**
100
* Builder for constraint violations with custom property paths and messages
101
*/
102
interface ConstraintViolationBuilder {
103
/**
104
* Add a node to the path the constraint violation will be associated to
105
* @param name property name
106
* @return updated builder
107
*/
108
NodeBuilderDefinedContext addPropertyNode(String name);
109
110
/**
111
* Add a bean node to the path
112
* @return updated builder
113
*/
114
NodeBuilderCustomizableContext addBeanNode();
115
116
/**
117
* Add a container element node to the path
118
* @param name container element name
119
* @param containerType container type
120
* @param typeArgumentIndex type argument index
121
* @return updated builder
122
*/
123
ContainerElementNodeBuilderDefinedContext addContainerElementNode(
124
String name, Class<?> containerType, Integer typeArgumentIndex);
125
126
/**
127
* Add the constraint violation built by this builder to the constraint violation list
128
* @return context for additional violations
129
*/
130
ConstraintValidatorContext addConstraintViolation();
131
132
/**
133
* Base interface for node builders
134
*/
135
interface NodeBuilderDefinedContext {
136
ConstraintViolationBuilder addConstraintViolation();
137
}
138
139
/**
140
* Customizable node builder context
141
*/
142
interface NodeBuilderCustomizableContext {
143
NodeContextBuilder inIterable();
144
ConstraintViolationBuilder addConstraintViolation();
145
}
146
147
/**
148
* Context for building node details
149
*/
150
interface NodeContextBuilder {
151
NodeBuilderDefinedContext atKey(Object key);
152
NodeBuilderDefinedContext atIndex(Integer index);
153
ConstraintViolationBuilder addConstraintViolation();
154
}
155
156
/**
157
* Container element node builder context
158
*/
159
interface ContainerElementNodeBuilderDefinedContext {
160
ContainerElementNodeContextBuilder inIterable();
161
ConstraintViolationBuilder addConstraintViolation();
162
}
163
164
/**
165
* Container element node context builder
166
*/
167
interface ContainerElementNodeContextBuilder {
168
ContainerElementNodeBuilderDefinedContext atKey(Object key);
169
ContainerElementNodeBuilderDefinedContext atIndex(Integer index);
170
ConstraintViolationBuilder addConstraintViolation();
171
}
172
}
173
}
174
```
175
176
### Cascading Validation
177
178
Annotation for marking properties, method parameters, or return values for validation cascading.
179
180
```java { .api }
181
/**
182
* Marks a property, method parameter or method return type for validation cascading
183
* Constraints defined on the object and its properties are validated
184
*/
185
@Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE})
186
@Retention(RUNTIME)
187
@interface Valid {}
188
```
189
190
### Composed Constraints
191
192
Annotations for creating composed constraints from multiple constraints.
193
194
```java { .api }
195
/**
196
* Marks a composed constraint as returning a single constraint violation report
197
* All constraint violations from composing constraints are ignored
198
*/
199
@Target({ANNOTATION_TYPE})
200
@Retention(RUNTIME)
201
@interface ReportAsSingleViolation {}
202
203
/**
204
* Marks a constraint attribute as overriding another constraint's attribute
205
* Used in composed constraints to override composing constraint attributes
206
*/
207
@Target({METHOD})
208
@Retention(RUNTIME)
209
@interface OverridesAttribute {
210
/**
211
* The constraint whose attribute this element overrides
212
* @return constraint class
213
*/
214
Class<? extends Annotation> constraint();
215
216
/**
217
* Name of the attribute to override
218
* @return attribute name
219
*/
220
String name();
221
}
222
```
223
224
**Usage Examples:**
225
226
```java
227
import jakarta.validation.*;
228
import jakarta.validation.constraints.*;
229
230
// 1. Simple custom constraint
231
@Target({ElementType.FIELD, ElementType.PARAMETER})
232
@Retention(RetentionPolicy.RUNTIME)
233
@Constraint(validatedBy = {PositiveEvenValidator.class})
234
public @interface PositiveEven {
235
String message() default "Must be a positive even number";
236
Class<?>[] groups() default {};
237
Class<? extends Payload>[] payload() default {};
238
}
239
240
// Validator implementation
241
public class PositiveEvenValidator implements ConstraintValidator<PositiveEven, Integer> {
242
@Override
243
public boolean isValid(Integer value, ConstraintValidatorContext context) {
244
if (value == null) {
245
return true; // Let @NotNull handle null validation
246
}
247
return value > 0 && value % 2 == 0;
248
}
249
}
250
251
// 2. Custom constraint with custom violation messages
252
@Target({ElementType.FIELD})
253
@Retention(RetentionPolicy.RUNTIME)
254
@Constraint(validatedBy = {PasswordValidator.class})
255
public @interface ValidPassword {
256
String message() default "Invalid password";
257
Class<?>[] groups() default {};
258
Class<? extends Payload>[] payload() default {};
259
260
boolean requireUppercase() default true;
261
boolean requireDigits() default true;
262
int minLength() default 8;
263
}
264
265
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
266
private boolean requireUppercase;
267
private boolean requireDigits;
268
private int minLength;
269
270
@Override
271
public void initialize(ValidPassword annotation) {
272
this.requireUppercase = annotation.requireUppercase();
273
this.requireDigits = annotation.requireDigits();
274
this.minLength = annotation.minLength();
275
}
276
277
@Override
278
public boolean isValid(String password, ConstraintValidatorContext context) {
279
if (password == null || password.length() < minLength) {
280
return false;
281
}
282
283
context.disableDefaultConstraintViolation();
284
boolean isValid = true;
285
286
if (requireUppercase && !password.matches(".*[A-Z].*")) {
287
context.buildConstraintViolationWithTemplate("Password must contain uppercase letter")
288
.addConstraintViolation();
289
isValid = false;
290
}
291
292
if (requireDigits && !password.matches(".*\\d.*")) {
293
context.buildConstraintViolationWithTemplate("Password must contain digit")
294
.addConstraintViolation();
295
isValid = false;
296
}
297
298
return isValid;
299
}
300
}
301
302
// 3. Composed constraint
303
@NotNull
304
@Size(min = 2, max = 50)
305
@Pattern(regexp = "^[A-Za-z ]+$")
306
@Target({ElementType.FIELD, ElementType.PARAMETER})
307
@Retention(RetentionPolicy.RUNTIME)
308
@Constraint(validatedBy = {})
309
@ReportAsSingleViolation
310
public @interface ValidName {
311
String message() default "Invalid name format";
312
Class<?>[] groups() default {};
313
Class<? extends Payload>[] payload() default {};
314
}
315
316
// 4. Cross-field validation
317
@Target({ElementType.TYPE})
318
@Retention(RetentionPolicy.RUNTIME)
319
@Constraint(validatedBy = {PasswordMatchesValidator.class})
320
public @interface PasswordMatches {
321
String message() default "Passwords don't match";
322
Class<?>[] groups() default {};
323
Class<? extends Payload>[] payload() default {};
324
}
325
326
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
327
@Override
328
public boolean isValid(Object obj, ConstraintValidatorContext context) {
329
// Assume obj has getPassword() and getConfirmPassword() methods
330
try {
331
String password = (String) obj.getClass().getMethod("getPassword").invoke(obj);
332
String confirmPassword = (String) obj.getClass().getMethod("getConfirmPassword").invoke(obj);
333
334
boolean matches = Objects.equals(password, confirmPassword);
335
336
if (!matches) {
337
context.disableDefaultConstraintViolation();
338
context.buildConstraintViolationWithTemplate("Passwords don't match")
339
.addPropertyNode("confirmPassword")
340
.addConstraintViolation();
341
}
342
343
return matches;
344
} catch (Exception e) {
345
return false;
346
}
347
}
348
}
349
350
// Usage in a class
351
@PasswordMatches
352
public class UserRegistration {
353
@ValidName
354
private String firstName;
355
356
@ValidPassword(minLength = 10, requireUppercase = true, requireDigits = true)
357
private String password;
358
359
private String confirmPassword;
360
361
@PositiveEven
362
private Integer luckyNumber;
363
364
// getters and setters...
365
}
366
```