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
```