or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-features.mdattributes.mdcore-immutable.mdindex.mdstyle-configuration.mdvalidation.md

validation.mddocs/

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

@Email

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

```