or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

error-handling.mdframework-configuration.mdhttp-caching.mdindex.mdjsr310-parameters.mdoptional-handling.mdparameter-handling.mdsession-management.mdvalidation.md

optional-handling.mddocs/

0

# Optional Handling

1

2

Support for Java 8 Optional and Guava Optional types with automatic serialization and 404 responses for empty values. Provides seamless integration between optional types and HTTP semantics in JAX-RS resources.

3

4

## Capabilities

5

6

### Java 8 Optional Support

7

8

Message body writers and exception handling for Java 8 Optional types with automatic HTTP status code mapping.

9

10

```java { .api }

11

/**

12

* Message body writer for Optional<?> types

13

* Returns 404 Not Found for empty Optional values

14

*/

15

public class OptionalMessageBodyWriter implements MessageBodyWriter<Optional<?>> {

16

17

/** Checks if this writer can handle the given Optional type */

18

public boolean isWriteable(Class<?> type, Type genericType,

19

Annotation[] annotations, MediaType mediaType);

20

21

/** Gets content length for Optional (returns -1 for chunked encoding) */

22

public long getSize(Optional<?> optional, Class<?> type, Type genericType,

23

Annotation[] annotations, MediaType mediaType);

24

25

/** Writes Optional content or throws EmptyOptionalException for empty values */

26

public void writeTo(Optional<?> optional, Class<?> type, Type genericType,

27

Annotation[] annotations, MediaType mediaType,

28

MultivaluedMap<String, Object> httpHeaders,

29

OutputStream entityStream) throws IOException, WebApplicationException;

30

}

31

32

/**

33

* Message body writer for OptionalInt primitive type

34

* Handles primitive int Optional values

35

*/

36

public class OptionalIntMessageBodyWriter implements MessageBodyWriter<OptionalInt> {

37

38

/** Checks if this writer can handle OptionalInt */

39

public boolean isWriteable(Class<?> type, Type genericType,

40

Annotation[] annotations, MediaType mediaType);

41

42

/** Writes OptionalInt value or throws exception for empty values */

43

public void writeTo(OptionalInt optionalInt, Class<?> type, Type genericType,

44

Annotation[] annotations, MediaType mediaType,

45

MultivaluedMap<String, Object> httpHeaders,

46

OutputStream entityStream) throws IOException, WebApplicationException;

47

}

48

49

/**

50

* Message body writer for OptionalLong primitive type

51

*/

52

public class OptionalLongMessageBodyWriter implements MessageBodyWriter<OptionalLong> {

53

54

public boolean isWriteable(Class<?> type, Type genericType,

55

Annotation[] annotations, MediaType mediaType);

56

57

public void writeTo(OptionalLong optionalLong, Class<?> type, Type genericType,

58

Annotation[] annotations, MediaType mediaType,

59

MultivaluedMap<String, Object> httpHeaders,

60

OutputStream entityStream) throws IOException, WebApplicationException;

61

}

62

63

/**

64

* Message body writer for OptionalDouble primitive type

65

*/

66

public class OptionalDoubleMessageBodyWriter implements MessageBodyWriter<OptionalDouble> {

67

68

public boolean isWriteable(Class<?> type, Type genericType,

69

Annotation[] annotations, MediaType mediaType);

70

71

public void writeTo(OptionalDouble optionalDouble, Class<?> type, Type genericType,

72

Annotation[] annotations, MediaType mediaType,

73

MultivaluedMap<String, Object> httpHeaders,

74

OutputStream entityStream) throws IOException, WebApplicationException;

75

}

76

```

77

78

**Usage Examples:**

79

80

```java

81

import java.util.Optional;

82

import java.util.OptionalInt;

83

import java.util.OptionalLong;

84

import java.util.OptionalDouble;

85

import jakarta.ws.rs.*;

86

87

@Path("/users")

88

public class UserResource {

89

90

@GET

91

@Path("/{id}")

92

public Optional<User> getUser(@PathParam("id") UUIDParam userId) {

93

// Returns 404 automatically if Optional is empty

94

return userService.findById(userId.get());

95

}

96

97

@GET

98

@Path("/{id}/age")

99

public OptionalInt getUserAge(@PathParam("id") UUIDParam userId) {

100

// Returns 404 if user not found or age not set

101

return userService.getUserAge(userId.get());

102

}

103

104

@GET

105

@Path("/{id}/salary")

106

public OptionalLong getUserSalary(@PathParam("id") UUIDParam userId) {

107

// Returns 404 if user not found or salary not set

108

return userService.getUserSalary(userId.get());

109

}

110

111

@GET

112

@Path("/{id}/rating")

113

public OptionalDouble getUserRating(@PathParam("id") UUIDParam userId) {

114

// Returns 404 if user not found or rating not set

115

return userService.getUserRating(userId.get());

116

}

117

}

118

```

119

120

### Empty Optional Exception Handling

121

122

Exception types and mappers for handling empty Optional values with different HTTP response strategies.

123

124

```java { .api }

125

/**

126

* Exception thrown when an Optional value is empty

127

* Used internally by Optional message body writers

128

*/

129

public class EmptyOptionalException extends WebApplicationException {

130

131

/** Creates exception with default message */

132

public EmptyOptionalException();

133

134

/** Creates exception with custom message */

135

public EmptyOptionalException(String message);

136

137

/** Creates exception with cause */

138

public EmptyOptionalException(Throwable cause);

139

140

/** Creates exception with message and cause */

141

public EmptyOptionalException(String message, Throwable cause);

142

}

143

144

/**

145

* Exception mapper that converts EmptyOptionalException to 404 Not Found

146

* Default behavior for empty Optional values

147

*/

148

public class EmptyOptionalExceptionMapper implements ExceptionMapper<EmptyOptionalException> {

149

150

/** Maps EmptyOptionalException to 404 Not Found response */

151

public Response toResponse(EmptyOptionalException exception);

152

}

153

154

/**

155

* Alternative exception mapper that converts EmptyOptionalException to 204 No Content

156

* Useful when empty values should return success with no content

157

*/

158

public class EmptyOptionalNoContentExceptionMapper implements ExceptionMapper<EmptyOptionalException> {

159

160

/** Maps EmptyOptionalException to 204 No Content response */

161

public Response toResponse(EmptyOptionalException exception);

162

}

163

```

164

165

### Guava Optional Support

166

167

Support for Google Guava Optional types for legacy code compatibility.

168

169

```java { .api }

170

/**

171

* Message body writer for Guava Optional types

172

* Provides compatibility with Google Guava Optional

173

*/

174

public class OptionalMessageBodyWriter implements MessageBodyWriter<com.google.common.base.Optional<?>> {

175

176

/** Checks if this writer can handle Guava Optional */

177

public boolean isWriteable(Class<?> type, Type genericType,

178

Annotation[] annotations, MediaType mediaType);

179

180

/** Writes Guava Optional content or returns 404 for absent values */

181

public void writeTo(com.google.common.base.Optional<?> optional, Class<?> type, Type genericType,

182

Annotation[] annotations, MediaType mediaType,

183

MultivaluedMap<String, Object> httpHeaders,

184

OutputStream entityStream) throws IOException, WebApplicationException;

185

}

186

187

/**

188

* Parameter binder for Guava Optional parameter converters

189

* Enables parameter conversion for Guava Optional types

190

*/

191

public class OptionalParamBinder extends AbstractBinder {

192

193

/** Configures Guava Optional parameter bindings */

194

protected void configure();

195

}

196

197

/**

198

* Parameter converter provider for Guava Optional types

199

* Provides automatic conversion of query/path parameters to Guava Optional

200

*/

201

public class OptionalParamConverterProvider implements ParamConverterProvider {

202

203

/** Gets parameter converter for Guava Optional types */

204

public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations);

205

}

206

```

207

208

**Guava Optional Usage:**

209

210

```java

211

import com.google.common.base.Optional;

212

import jakarta.ws.rs.*;

213

214

@Path("/legacy")

215

public class LegacyResource {

216

217

@GET

218

@Path("/users/{id}")

219

public Optional<User> getUser(@PathParam("id") UUIDParam userId) {

220

// Using Guava Optional for legacy compatibility

221

User user = userService.findById(userId.get());

222

return Optional.fromNullable(user);

223

}

224

225

@GET

226

@Path("/search")

227

public List<User> searchUsers(@QueryParam("name") Optional<String> name,

228

@QueryParam("email") Optional<String> email) {

229

// Guava Optional parameters

230

String nameFilter = name.orNull();

231

String emailFilter = email.orNull();

232

return userService.search(nameFilter, emailFilter);

233

}

234

}

235

```

236

237

## Optional Response Patterns

238

239

### Standard Optional Usage

240

241

```java

242

@Path("/api")

243

public class OptionalResponseExamples {

244

245

@GET

246

@Path("/users/{id}")

247

public Optional<User> getUser(@PathParam("id") UUIDParam userId) {

248

// Service returns Optional - automatically converts to 404 if empty

249

return userService.findById(userId.get());

250

}

251

252

@GET

253

@Path("/users/{id}/profile")

254

public Optional<UserProfile> getUserProfile(@PathParam("id") UUIDParam userId) {

255

// Nested service calls with Optional chaining

256

return userService.findById(userId.get())

257

.flatMap(user -> profileService.getProfile(user.getId()));

258

}

259

260

@GET

261

@Path("/users/{id}/preferences")

262

public Optional<UserPreferences> getUserPreferences(@PathParam("id") UUIDParam userId) {

263

// Returns 404 if user not found or preferences not set

264

return userService.findById(userId.get())

265

.map(user -> user.getPreferences())

266

.filter(prefs -> prefs != null);

267

}

268

}

269

```

270

271

### Conditional Optional Responses

272

273

```java

274

@Path("/conditional")

275

public class ConditionalOptionalResource {

276

277

@GET

278

@Path("/data/{id}")

279

public Optional<Data> getData(@PathParam("id") UUIDParam dataId,

280

@QueryParam("includeDetails") boolean includeDetails) {

281

Optional<Data> data = dataService.findById(dataId.get());

282

283

if (includeDetails) {

284

// Include additional details if requested

285

return data.map(d -> dataService.enrichWithDetails(d));

286

}

287

288

return data;

289

}

290

291

@GET

292

@Path("/users/{id}/stats")

293

public Optional<UserStats> getUserStats(@PathParam("id") UUIDParam userId,

294

@QueryParam("period") String period) {

295

return userService.findById(userId.get())

296

.flatMap(user -> {

297

if ("month".equals(period)) {

298

return statsService.getMonthlyStats(user.getId());

299

} else if ("year".equals(period)) {

300

return statsService.getYearlyStats(user.getId());

301

} else {

302

return statsService.getAllTimeStats(user.getId());

303

}

304

});

305

}

306

}

307

```

308

309

### Optional with Custom HTTP Status

310

311

```java

312

@Path("/custom")

313

public class CustomOptionalResource {

314

315

@GET

316

@Path("/data/{id}")

317

public Response getDataWithCustomStatus(@PathParam("id") UUIDParam dataId) {

318

Optional<Data> data = dataService.findById(dataId.get());

319

320

if (data.isPresent()) {

321

return Response.ok(data.get()).build();

322

} else {

323

// Custom response for empty Optional

324

return Response.status(204) // No Content instead of 404

325

.header("X-Reason", "Data not available")

326

.build();

327

}

328

}

329

330

@GET

331

@Path("/users/{id}/avatar")

332

public Response getUserAvatar(@PathParam("id") UUIDParam userId) {

333

Optional<byte[]> avatar = userService.getAvatar(userId.get());

334

335

if (avatar.isPresent()) {

336

return Response.ok(avatar.get())

337

.type("image/png")

338

.build();

339

} else {

340

// Redirect to default avatar instead of 404

341

return Response.seeOther(URI.create("/images/default-avatar.png")).build();

342

}

343

}

344

}

345

```

346

347

## Configuration and Customization

348

349

### Configuring Optional Behavior

350

351

```java

352

public class OptionalConfiguration {

353

354

public void configureOptionalHandling(JerseyEnvironment jersey) {

355

// Use 204 No Content instead of 404 for empty Optionals

356

jersey.register(EmptyOptionalNoContentExceptionMapper.class);

357

358

// Or keep default 404 behavior

359

// jersey.register(EmptyOptionalExceptionMapper.class); // Default

360

361

// Register optional message body writers (automatically registered by DropwizardResourceConfig)

362

jersey.register(OptionalMessageBodyWriter.class);

363

jersey.register(OptionalIntMessageBodyWriter.class);

364

jersey.register(OptionalLongMessageBodyWriter.class);

365

jersey.register(OptionalDoubleMessageBodyWriter.class);

366

}

367

368

public void configureGuavaOptional(JerseyEnvironment jersey) {

369

// Enable Guava Optional support

370

jersey.register(io.dropwizard.jersey.guava.OptionalMessageBodyWriter.class);

371

jersey.register(new OptionalParamBinder());

372

}

373

}

374

```

375

376

### Custom Optional Exception Mapper

377

378

```java

379

@Provider

380

public class CustomOptionalExceptionMapper implements ExceptionMapper<EmptyOptionalException> {

381

382

@Override

383

public Response toResponse(EmptyOptionalException exception) {

384

// Custom logic based on request context

385

UriInfo uriInfo = getUriInfo(); // Inject UriInfo

386

String path = uriInfo.getPath();

387

388

if (path.contains("/users/")) {

389

return Response.status(404)

390

.entity(new ErrorMessage(404, "User not found"))

391

.build();

392

} else if (path.contains("/data/")) {

393

return Response.status(204)

394

.header("X-Data-Status", "Not Available")

395

.build();

396

} else {

397

return Response.status(404)

398

.entity(new ErrorMessage(404, "Resource not found"))

399

.build();

400

}

401

}

402

}

403

```

404

405

## Best Practices

406

407

### When to Use Optional

408

409

```java

410

public class OptionalBestPractices {

411

412

// Good: Use Optional for single resource lookups that may not exist

413

@GET

414

@Path("/users/{id}")

415

public Optional<User> getUser(@PathParam("id") UUIDParam userId) {

416

return userService.findById(userId.get());

417

}

418

419

// Good: Use Optional for optional resource properties

420

@GET

421

@Path("/users/{id}/avatar")

422

public Optional<byte[]> getUserAvatar(@PathParam("id") UUIDParam userId) {

423

return userService.getAvatar(userId.get());

424

}

425

426

// Avoid: Don't use Optional for collections - return empty collections instead

427

@GET

428

@Path("/users")

429

public List<User> getUsers() {

430

return userService.getAllUsers(); // Returns empty list, not Optional<List<User>>

431

}

432

433

// Good: Use primitive Optionals for numeric values that may be absent

434

@GET

435

@Path("/users/{id}/age")

436

public OptionalInt getUserAge(@PathParam("id") UUIDParam userId) {

437

return userService.getUserAge(userId.get());

438

}

439

}

440

```

441

442

### Optional Parameter Handling

443

444

```java

445

@Path("/search")

446

public class OptionalParameterResource {

447

448

@GET

449

public List<User> searchUsers(@QueryParam("name") Optional<String> name,

450

@QueryParam("minAge") OptionalInt minAge,

451

@QueryParam("maxAge") OptionalInt maxAge) {

452

453

SearchCriteria criteria = new SearchCriteria();

454

455

// Handle optional parameters

456

name.ifPresent(criteria::setName);

457

minAge.ifPresent(criteria::setMinAge);

458

maxAge.ifPresent(criteria::setMaxAge);

459

460

return userService.search(criteria);

461

}

462

463

// Alternative approach with null checks

464

@GET

465

@Path("/alt")

466

public List<User> searchUsersAlt(@QueryParam("name") String name,

467

@QueryParam("minAge") Integer minAge,

468

@QueryParam("maxAge") Integer maxAge) {

469

470

SearchCriteria criteria = new SearchCriteria();

471

472

if (name != null) criteria.setName(name);

473

if (minAge != null) criteria.setMinAge(minAge);

474

if (maxAge != null) criteria.setMaxAge(maxAge);

475

476

return userService.search(criteria);

477

}

478

}

479

```

480

481

### Optional Chaining

482

483

```java

484

public class OptionalChainingExamples {

485

486

@GET

487

@Path("/users/{id}/organization/name")

488

public Optional<String> getUserOrganizationName(@PathParam("id") UUIDParam userId) {

489

// Chain optional operations

490

return userService.findById(userId.get())

491

.flatMap(user -> organizationService.findById(user.getOrganizationId()))

492

.map(org -> org.getName());

493

}

494

495

@GET

496

@Path("/orders/{id}/customer/email")

497

public Optional<String> getOrderCustomerEmail(@PathParam("id") UUIDParam orderId) {

498

return orderService.findById(orderId.get())

499

.flatMap(order -> customerService.findById(order.getCustomerId()))

500

.map(customer -> customer.getEmail())

501

.filter(email -> email != null && !email.isEmpty());

502

}

503

}

504

```