or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

binary-attachments.mdconvenience-api.mdcore-binding.mddata-type-conversion.mdindex.mdtransform-integration.mdtype-adapters.mdvalidation-error-handling.mdxml-mapping-annotations.md

type-adapters.mddocs/

0

# Type Adapters

1

2

Jakarta XML Binding type adapters provide a framework for custom type conversions during XML binding operations. They enable transformation of complex Java types to XML-compatible representations and vice versa, allowing for custom serialization logic while maintaining type safety.

3

4

## Capabilities

5

6

### XmlAdapter Framework

7

8

The core adapter framework provides the foundation for all custom type conversions.

9

10

```java { .api }

11

public abstract class XmlAdapter<ValueType, BoundType> {

12

protected XmlAdapter() {}

13

14

// Convert from bound type to value type (for marshalling)

15

public abstract ValueType marshal(BoundType value) throws Exception;

16

17

// Convert from value type to bound type (for unmarshalling)

18

public abstract BoundType unmarshal(ValueType value) throws Exception;

19

}

20

```

21

22

**Type Parameters:**

23

- `ValueType`: The type that JAXB knows how to handle (XML-compatible type)

24

- `BoundType`: The type that appears in your Java classes (domain-specific type)

25

26

**Usage Example:**

27

28

```java

29

// Adapter for converting between LocalDate and String

30

public class LocalDateAdapter extends XmlAdapter<String, LocalDate> {

31

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;

32

33

@Override

34

public LocalDate unmarshal(String value) throws Exception {

35

return value != null ? LocalDate.parse(value, FORMATTER) : null;

36

}

37

38

@Override

39

public String marshal(LocalDate value) throws Exception {

40

return value != null ? value.format(FORMATTER) : null;

41

}

42

}

43

```

44

45

### Adapter Registration

46

47

Annotations for applying type adapters to specific properties, types, or packages.

48

49

```java { .api }

50

@Target({

51

ElementType.PACKAGE,

52

ElementType.FIELD,

53

ElementType.METHOD,

54

ElementType.TYPE,

55

ElementType.PARAMETER

56

})

57

@Retention(RetentionPolicy.RUNTIME)

58

public @interface XmlJavaTypeAdapter {

59

Class<? extends XmlAdapter> value();

60

Class<?> type() default DEFAULT.class;

61

62

public static final class DEFAULT {}

63

}

64

65

@Target({ElementType.PACKAGE})

66

@Retention(RetentionPolicy.RUNTIME)

67

public @interface XmlJavaTypeAdapters {

68

XmlJavaTypeAdapter[] value();

69

}

70

```

71

72

**Usage Examples:**

73

74

```java

75

public class Person {

76

// Apply adapter to specific field

77

@XmlJavaTypeAdapter(LocalDateAdapter.class)

78

private LocalDate birthDate;

79

80

// Apply adapter with explicit type

81

@XmlJavaTypeAdapter(value = UUIDAdapter.class, type = UUID.class)

82

private UUID identifier;

83

}

84

85

// Apply adapter to all uses of a type in a class

86

@XmlJavaTypeAdapter(value = LocalDateAdapter.class, type = LocalDate.class)

87

public class Employee {

88

private LocalDate hireDate; // Uses LocalDateAdapter

89

private LocalDate lastReview; // Uses LocalDateAdapter

90

}

91

92

// Package-level adapter registration

93

@XmlJavaTypeAdapters({

94

@XmlJavaTypeAdapter(value = LocalDateAdapter.class, type = LocalDate.class),

95

@XmlJavaTypeAdapter(value = UUIDAdapter.class, type = UUID.class),

96

@XmlJavaTypeAdapter(value = MoneyAdapter.class, type = BigDecimal.class)

97

})

98

package com.company.model;

99

```

100

101

### Built-in Adapters

102

103

Jakarta XML Binding provides several built-in adapters for common string processing scenarios.

104

105

```java { .api }

106

public final class NormalizedStringAdapter extends XmlAdapter<String, String> {

107

public String unmarshal(String text);

108

public String marshal(String s);

109

}

110

111

public class CollapsedStringAdapter extends XmlAdapter<String, String> {

112

public String unmarshal(String text);

113

public String marshal(String s);

114

}

115

116

public final class HexBinaryAdapter extends XmlAdapter<String, byte[]> {

117

public byte[] unmarshal(String s);

118

public String marshal(byte[] bytes);

119

}

120

```

121

122

**Built-in Adapter Characteristics:**

123

124

- **NormalizedStringAdapter**: Normalizes line endings and replaces tabs with spaces

125

- **CollapsedStringAdapter**: Trims leading/trailing whitespace and collapses internal whitespace

126

- **HexBinaryAdapter**: Converts between byte arrays and hexadecimal string representation

127

128

**Usage Examples:**

129

130

```java

131

public class Document {

132

// Normalize whitespace in comments

133

@XmlJavaTypeAdapter(NormalizedStringAdapter.class)

134

private String comments;

135

136

// Collapse whitespace in names

137

@XmlJavaTypeAdapter(CollapsedStringAdapter.class)

138

private String displayName;

139

140

// Handle binary data as hex strings

141

@XmlJavaTypeAdapter(HexBinaryAdapter.class)

142

private byte[] checksum;

143

}

144

```

145

146

## Common Adapter Patterns

147

148

### Date and Time Adapters

149

150

Custom adapters for modern Java date/time types.

151

152

```java

153

// LocalDateTime adapter

154

public class LocalDateTimeAdapter extends XmlAdapter<String, LocalDateTime> {

155

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

156

157

@Override

158

public LocalDateTime unmarshal(String value) throws Exception {

159

return value != null ? LocalDateTime.parse(value, FORMATTER) : null;

160

}

161

162

@Override

163

public String marshal(LocalDateTime value) throws Exception {

164

return value != null ? value.format(FORMATTER) : null;

165

}

166

}

167

168

// ZonedDateTime adapter with timezone handling

169

public class ZonedDateTimeAdapter extends XmlAdapter<String, ZonedDateTime> {

170

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME;

171

172

@Override

173

public ZonedDateTime unmarshal(String value) throws Exception {

174

return value != null ? ZonedDateTime.parse(value, FORMATTER) : null;

175

}

176

177

@Override

178

public String marshal(ZonedDateTime value) throws Exception {

179

return value != null ? value.format(FORMATTER) : null;

180

}

181

}

182

183

// Usage

184

public class Event {

185

@XmlJavaTypeAdapter(LocalDateTimeAdapter.class)

186

private LocalDateTime startTime;

187

188

@XmlJavaTypeAdapter(ZonedDateTimeAdapter.class)

189

private ZonedDateTime scheduledTime;

190

}

191

```

192

193

### Enum and Complex Type Adapters

194

195

Adapters for custom enum serialization and complex object transformations.

196

197

```java

198

// Custom enum adapter

199

public class StatusAdapter extends XmlAdapter<String, Status> {

200

@Override

201

public Status unmarshal(String value) throws Exception {

202

if (value == null) return null;

203

204

switch (value.toLowerCase()) {

205

case "1": case "active": return Status.ACTIVE;

206

case "0": case "inactive": return Status.INACTIVE;

207

case "pending": case "wait": return Status.PENDING;

208

default: throw new IllegalArgumentException("Unknown status: " + value);

209

}

210

}

211

212

@Override

213

public String marshal(Status value) throws Exception {

214

if (value == null) return null;

215

216

switch (value) {

217

case ACTIVE: return "active";

218

case INACTIVE: return "inactive";

219

case PENDING: return "pending";

220

default: return value.name().toLowerCase();

221

}

222

}

223

}

224

225

// Complex object adapter

226

public class AddressAdapter extends XmlAdapter<String, Address> {

227

@Override

228

public Address unmarshal(String value) throws Exception {

229

if (value == null || value.trim().isEmpty()) return null;

230

231

// Parse "123 Main St, Anytown, ST 12345" format

232

String[] parts = value.split(",");

233

if (parts.length >= 3) {

234

return new Address(

235

parts[0].trim(), // street

236

parts[1].trim(), // city

237

parts[2].trim().split("\\s+")[0], // state

238

parts[2].trim().split("\\s+")[1] // zip

239

);

240

}

241

throw new IllegalArgumentException("Invalid address format: " + value);

242

}

243

244

@Override

245

public String marshal(Address value) throws Exception {

246

if (value == null) return null;

247

248

return String.format("%s, %s, %s %s",

249

value.getStreet(),

250

value.getCity(),

251

value.getState(),

252

value.getZipCode()

253

);

254

}

255

}

256

```

257

258

### Collection and Map Adapters

259

260

Adapters for custom collection serialization formats.

261

262

```java

263

// Map adapter for key-value pairs

264

public class StringMapAdapter extends XmlAdapter<StringMapAdapter.StringMap, Map<String, String>> {

265

266

public static class StringMap {

267

@XmlElement(name = "entry")

268

public List<Entry> entries = new ArrayList<>();

269

270

public static class Entry {

271

@XmlAttribute

272

public String key;

273

274

@XmlValue

275

public String value;

276

}

277

}

278

279

@Override

280

public Map<String, String> unmarshal(StringMap value) throws Exception {

281

if (value == null) return null;

282

283

Map<String, String> map = new HashMap<>();

284

for (StringMap.Entry entry : value.entries) {

285

map.put(entry.key, entry.value);

286

}

287

return map;

288

}

289

290

@Override

291

public StringMap marshal(Map<String, String> value) throws Exception {

292

if (value == null) return null;

293

294

StringMap result = new StringMap();

295

for (Map.Entry<String, String> entry : value.entrySet()) {

296

StringMap.Entry xmlEntry = new StringMap.Entry();

297

xmlEntry.key = entry.getKey();

298

xmlEntry.value = entry.getValue();

299

result.entries.add(xmlEntry);

300

}

301

return result;

302

}

303

}

304

305

// Usage

306

public class Configuration {

307

@XmlJavaTypeAdapter(StringMapAdapter.class)

308

private Map<String, String> properties;

309

}

310

```

311

312

### Validation and Error Handling

313

314

Adapters with validation and comprehensive error handling.

315

316

```java

317

public class EmailAdapter extends XmlAdapter<String, Email> {

318

private static final Pattern EMAIL_PATTERN = Pattern.compile(

319

"^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$"

320

);

321

322

@Override

323

public Email unmarshal(String value) throws Exception {

324

if (value == null || value.trim().isEmpty()) {

325

return null;

326

}

327

328

String trimmed = value.trim();

329

if (!EMAIL_PATTERN.matcher(trimmed).matches()) {

330

throw new IllegalArgumentException("Invalid email format: " + value);

331

}

332

333

return new Email(trimmed);

334

}

335

336

@Override

337

public String marshal(Email value) throws Exception {

338

return value != null ? value.getAddress() : null;

339

}

340

}

341

342

// Currency adapter with validation

343

public class CurrencyAdapter extends XmlAdapter<String, BigDecimal> {

344

private static final Pattern CURRENCY_PATTERN = Pattern.compile("^\\$?([0-9]{1,3}(,[0-9]{3})*|[0-9]+)(\\.[0-9]{2})?$");

345

346

@Override

347

public BigDecimal unmarshal(String value) throws Exception {

348

if (value == null || value.trim().isEmpty()) {

349

return null;

350

}

351

352

String cleaned = value.replaceAll("[$,]", "");

353

354

if (!CURRENCY_PATTERN.matcher(value).matches()) {

355

throw new NumberFormatException("Invalid currency format: " + value);

356

}

357

358

return new BigDecimal(cleaned);

359

}

360

361

@Override

362

public String marshal(BigDecimal value) throws Exception {

363

if (value == null) return null;

364

365

NumberFormat formatter = NumberFormat.getCurrencyInstance();

366

return formatter.format(value);

367

}

368

}

369

```

370

371

## Runtime Adapter Management

372

373

Marshaller and Unmarshaller interfaces provide methods for runtime adapter management.

374

375

```java { .api }

376

// In Marshaller and Unmarshaller interfaces

377

public interface Marshaller {

378

<A extends XmlAdapter> void setAdapter(Class<A> type, A adapter);

379

<A extends XmlAdapter> A getAdapter(Class<A> type);

380

void setAdapter(XmlAdapter adapter);

381

}

382

383

public interface Unmarshaller {

384

<A extends XmlAdapter> void setAdapter(Class<A> type, A adapter);

385

<A extends XmlAdapter> A getAdapter(Class<A> type);

386

void setAdapter(XmlAdapter adapter);

387

}

388

```

389

390

**Usage Examples:**

391

392

```java

393

JAXBContext context = JAXBContext.newInstance(Person.class);

394

Marshaller marshaller = context.createMarshaller();

395

396

// Set specific adapter instance

397

LocalDateAdapter dateAdapter = new LocalDateAdapter();

398

marshaller.setAdapter(LocalDateAdapter.class, dateAdapter);

399

400

// Set adapter by instance (type inferred)

401

marshaller.setAdapter(new CurrencyAdapter());

402

403

// Get current adapter

404

LocalDateAdapter currentAdapter = marshaller.getAdapter(LocalDateAdapter.class);

405

406

// Apply to unmarshaller as well

407

Unmarshaller unmarshaller = context.createUnmarshaller();

408

unmarshaller.setAdapter(LocalDateAdapter.class, dateAdapter);

409

```

410

411

## Best Practices

412

413

### Thread Safety

414

415

```java

416

// Thread-safe adapter (stateless)

417

public class ThreadSafeAdapter extends XmlAdapter<String, LocalDate> {

418

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;

419

420

@Override

421

public LocalDate unmarshal(String value) throws Exception {

422

// No instance state - thread safe

423

return value != null ? LocalDate.parse(value, FORMATTER) : null;

424

}

425

426

@Override

427

public String marshal(LocalDate value) throws Exception {

428

return value != null ? value.format(FORMATTER) : null;

429

}

430

}

431

432

// Thread-unsafe adapter (with state)

433

public class ConfigurableAdapter extends XmlAdapter<String, LocalDate> {

434

private DateTimeFormatter formatter; // Instance state

435

436

public ConfigurableAdapter(String pattern) {

437

this.formatter = DateTimeFormatter.ofPattern(pattern);

438

}

439

440

// This adapter is not thread-safe due to mutable state

441

// Each thread should have its own instance

442

}

443

```

444

445

### Error Handling and Logging

446

447

```java

448

public class RobustAdapter extends XmlAdapter<String, CustomType> {

449

private static final Logger logger = LoggerFactory.getLogger(RobustAdapter.class);

450

451

@Override

452

public CustomType unmarshal(String value) throws Exception {

453

try {

454

if (value == null || value.trim().isEmpty()) {

455

return null;

456

}

457

458

// Conversion logic

459

CustomType result = parseCustomType(value);

460

logger.debug("Successfully unmarshalled: {} -> {}", value, result);

461

return result;

462

463

} catch (Exception e) {

464

logger.error("Failed to unmarshal value: {}", value, e);

465

throw new IllegalArgumentException("Invalid format for CustomType: " + value, e);

466

}

467

}

468

469

@Override

470

public String marshal(CustomType value) throws Exception {

471

try {

472

if (value == null) {

473

return null;

474

}

475

476

String result = formatCustomType(value);

477

logger.debug("Successfully marshalled: {} -> {}", value, result);

478

return result;

479

480

} catch (Exception e) {

481

logger.error("Failed to marshal value: {}", value, e);

482

throw new IllegalStateException("Cannot format CustomType: " + value, e);

483

}

484

}

485

486

private CustomType parseCustomType(String value) throws Exception {

487

// Implementation details

488

}

489

490

private String formatCustomType(CustomType value) throws Exception {

491

// Implementation details

492

}

493

}

494

```

495

496

### Null Handling

497

498

```java

499

public class NullSafeAdapter extends XmlAdapter<String, Optional<LocalDate>> {

500

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;

501

502

@Override

503

public Optional<LocalDate> unmarshal(String value) throws Exception {

504

// Handle null and empty values gracefully

505

if (value == null || value.trim().isEmpty()) {

506

return Optional.empty();

507

}

508

509

try {

510

return Optional.of(LocalDate.parse(value.trim(), FORMATTER));

511

} catch (DateTimeParseException e) {

512

// Log warning but don't fail - return empty optional

513

return Optional.empty();

514

}

515

}

516

517

@Override

518

public String marshal(Optional<LocalDate> value) throws Exception {

519

// Handle Optional container

520

return value != null && value.isPresent()

521

? value.get().format(FORMATTER)

522

: null;

523

}

524

}

525

```