or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

bean-validation.mdbootstrap-configuration.mdconstraints.mdcontainer-validation.mdcustom-constraints.mdindex.mdmetadata.mdmethod-validation.mdvalidation-groups.md

custom-constraints.mddocs/

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

```