Jakarta XML Binding API that automates the mapping between XML documents and Java objects through data binding
—
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.
The core adapter framework provides the foundation for all custom type conversions.
public abstract class XmlAdapter<ValueType, BoundType> {
protected XmlAdapter() {}
// Convert from bound type to value type (for marshalling)
public abstract ValueType marshal(BoundType value) throws Exception;
// Convert from value type to bound type (for unmarshalling)
public abstract BoundType unmarshal(ValueType value) throws Exception;
}Type Parameters:
ValueType: The type that JAXB knows how to handle (XML-compatible type)BoundType: The type that appears in your Java classes (domain-specific type)Usage Example:
// Adapter for converting between LocalDate and String
public class LocalDateAdapter extends XmlAdapter<String, LocalDate> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
@Override
public LocalDate unmarshal(String value) throws Exception {
return value != null ? LocalDate.parse(value, FORMATTER) : null;
}
@Override
public String marshal(LocalDate value) throws Exception {
return value != null ? value.format(FORMATTER) : null;
}
}Annotations for applying type adapters to specific properties, types, or packages.
@Target({
ElementType.PACKAGE,
ElementType.FIELD,
ElementType.METHOD,
ElementType.TYPE,
ElementType.PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
public @interface XmlJavaTypeAdapter {
Class<? extends XmlAdapter> value();
Class<?> type() default DEFAULT.class;
public static final class DEFAULT {}
}
@Target({ElementType.PACKAGE})
@Retention(RetentionPolicy.RUNTIME)
public @interface XmlJavaTypeAdapters {
XmlJavaTypeAdapter[] value();
}Usage Examples:
public class Person {
// Apply adapter to specific field
@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate birthDate;
// Apply adapter with explicit type
@XmlJavaTypeAdapter(value = UUIDAdapter.class, type = UUID.class)
private UUID identifier;
}
// Apply adapter to all uses of a type in a class
@XmlJavaTypeAdapter(value = LocalDateAdapter.class, type = LocalDate.class)
public class Employee {
private LocalDate hireDate; // Uses LocalDateAdapter
private LocalDate lastReview; // Uses LocalDateAdapter
}
// Package-level adapter registration
@XmlJavaTypeAdapters({
@XmlJavaTypeAdapter(value = LocalDateAdapter.class, type = LocalDate.class),
@XmlJavaTypeAdapter(value = UUIDAdapter.class, type = UUID.class),
@XmlJavaTypeAdapter(value = MoneyAdapter.class, type = BigDecimal.class)
})
package com.company.model;Jakarta XML Binding provides several built-in adapters for common string processing scenarios.
public final class NormalizedStringAdapter extends XmlAdapter<String, String> {
public String unmarshal(String text);
public String marshal(String s);
}
public class CollapsedStringAdapter extends XmlAdapter<String, String> {
public String unmarshal(String text);
public String marshal(String s);
}
public final class HexBinaryAdapter extends XmlAdapter<String, byte[]> {
public byte[] unmarshal(String s);
public String marshal(byte[] bytes);
}Built-in Adapter Characteristics:
Usage Examples:
public class Document {
// Normalize whitespace in comments
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
private String comments;
// Collapse whitespace in names
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
private String displayName;
// Handle binary data as hex strings
@XmlJavaTypeAdapter(HexBinaryAdapter.class)
private byte[] checksum;
}Custom adapters for modern Java date/time types.
// LocalDateTime adapter
public class LocalDateTimeAdapter extends XmlAdapter<String, LocalDateTime> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
@Override
public LocalDateTime unmarshal(String value) throws Exception {
return value != null ? LocalDateTime.parse(value, FORMATTER) : null;
}
@Override
public String marshal(LocalDateTime value) throws Exception {
return value != null ? value.format(FORMATTER) : null;
}
}
// ZonedDateTime adapter with timezone handling
public class ZonedDateTimeAdapter extends XmlAdapter<String, ZonedDateTime> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME;
@Override
public ZonedDateTime unmarshal(String value) throws Exception {
return value != null ? ZonedDateTime.parse(value, FORMATTER) : null;
}
@Override
public String marshal(ZonedDateTime value) throws Exception {
return value != null ? value.format(FORMATTER) : null;
}
}
// Usage
public class Event {
@XmlJavaTypeAdapter(LocalDateTimeAdapter.class)
private LocalDateTime startTime;
@XmlJavaTypeAdapter(ZonedDateTimeAdapter.class)
private ZonedDateTime scheduledTime;
}Adapters for custom enum serialization and complex object transformations.
// Custom enum adapter
public class StatusAdapter extends XmlAdapter<String, Status> {
@Override
public Status unmarshal(String value) throws Exception {
if (value == null) return null;
switch (value.toLowerCase()) {
case "1": case "active": return Status.ACTIVE;
case "0": case "inactive": return Status.INACTIVE;
case "pending": case "wait": return Status.PENDING;
default: throw new IllegalArgumentException("Unknown status: " + value);
}
}
@Override
public String marshal(Status value) throws Exception {
if (value == null) return null;
switch (value) {
case ACTIVE: return "active";
case INACTIVE: return "inactive";
case PENDING: return "pending";
default: return value.name().toLowerCase();
}
}
}
// Complex object adapter
public class AddressAdapter extends XmlAdapter<String, Address> {
@Override
public Address unmarshal(String value) throws Exception {
if (value == null || value.trim().isEmpty()) return null;
// Parse "123 Main St, Anytown, ST 12345" format
String[] parts = value.split(",");
if (parts.length >= 3) {
return new Address(
parts[0].trim(), // street
parts[1].trim(), // city
parts[2].trim().split("\\s+")[0], // state
parts[2].trim().split("\\s+")[1] // zip
);
}
throw new IllegalArgumentException("Invalid address format: " + value);
}
@Override
public String marshal(Address value) throws Exception {
if (value == null) return null;
return String.format("%s, %s, %s %s",
value.getStreet(),
value.getCity(),
value.getState(),
value.getZipCode()
);
}
}Adapters for custom collection serialization formats.
// Map adapter for key-value pairs
public class StringMapAdapter extends XmlAdapter<StringMapAdapter.StringMap, Map<String, String>> {
public static class StringMap {
@XmlElement(name = "entry")
public List<Entry> entries = new ArrayList<>();
public static class Entry {
@XmlAttribute
public String key;
@XmlValue
public String value;
}
}
@Override
public Map<String, String> unmarshal(StringMap value) throws Exception {
if (value == null) return null;
Map<String, String> map = new HashMap<>();
for (StringMap.Entry entry : value.entries) {
map.put(entry.key, entry.value);
}
return map;
}
@Override
public StringMap marshal(Map<String, String> value) throws Exception {
if (value == null) return null;
StringMap result = new StringMap();
for (Map.Entry<String, String> entry : value.entrySet()) {
StringMap.Entry xmlEntry = new StringMap.Entry();
xmlEntry.key = entry.getKey();
xmlEntry.value = entry.getValue();
result.entries.add(xmlEntry);
}
return result;
}
}
// Usage
public class Configuration {
@XmlJavaTypeAdapter(StringMapAdapter.class)
private Map<String, String> properties;
}Adapters with validation and comprehensive error handling.
public class EmailAdapter extends XmlAdapter<String, Email> {
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$"
);
@Override
public Email unmarshal(String value) throws Exception {
if (value == null || value.trim().isEmpty()) {
return null;
}
String trimmed = value.trim();
if (!EMAIL_PATTERN.matcher(trimmed).matches()) {
throw new IllegalArgumentException("Invalid email format: " + value);
}
return new Email(trimmed);
}
@Override
public String marshal(Email value) throws Exception {
return value != null ? value.getAddress() : null;
}
}
// Currency adapter with validation
public class CurrencyAdapter extends XmlAdapter<String, BigDecimal> {
private static final Pattern CURRENCY_PATTERN = Pattern.compile("^\\$?([0-9]{1,3}(,[0-9]{3})*|[0-9]+)(\\.[0-9]{2})?$");
@Override
public BigDecimal unmarshal(String value) throws Exception {
if (value == null || value.trim().isEmpty()) {
return null;
}
String cleaned = value.replaceAll("[$,]", "");
if (!CURRENCY_PATTERN.matcher(value).matches()) {
throw new NumberFormatException("Invalid currency format: " + value);
}
return new BigDecimal(cleaned);
}
@Override
public String marshal(BigDecimal value) throws Exception {
if (value == null) return null;
NumberFormat formatter = NumberFormat.getCurrencyInstance();
return formatter.format(value);
}
}Marshaller and Unmarshaller interfaces provide methods for runtime adapter management.
// In Marshaller and Unmarshaller interfaces
public interface Marshaller {
<A extends XmlAdapter> void setAdapter(Class<A> type, A adapter);
<A extends XmlAdapter> A getAdapter(Class<A> type);
void setAdapter(XmlAdapter adapter);
}
public interface Unmarshaller {
<A extends XmlAdapter> void setAdapter(Class<A> type, A adapter);
<A extends XmlAdapter> A getAdapter(Class<A> type);
void setAdapter(XmlAdapter adapter);
}Usage Examples:
JAXBContext context = JAXBContext.newInstance(Person.class);
Marshaller marshaller = context.createMarshaller();
// Set specific adapter instance
LocalDateAdapter dateAdapter = new LocalDateAdapter();
marshaller.setAdapter(LocalDateAdapter.class, dateAdapter);
// Set adapter by instance (type inferred)
marshaller.setAdapter(new CurrencyAdapter());
// Get current adapter
LocalDateAdapter currentAdapter = marshaller.getAdapter(LocalDateAdapter.class);
// Apply to unmarshaller as well
Unmarshaller unmarshaller = context.createUnmarshaller();
unmarshaller.setAdapter(LocalDateAdapter.class, dateAdapter);// Thread-safe adapter (stateless)
public class ThreadSafeAdapter extends XmlAdapter<String, LocalDate> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
@Override
public LocalDate unmarshal(String value) throws Exception {
// No instance state - thread safe
return value != null ? LocalDate.parse(value, FORMATTER) : null;
}
@Override
public String marshal(LocalDate value) throws Exception {
return value != null ? value.format(FORMATTER) : null;
}
}
// Thread-unsafe adapter (with state)
public class ConfigurableAdapter extends XmlAdapter<String, LocalDate> {
private DateTimeFormatter formatter; // Instance state
public ConfigurableAdapter(String pattern) {
this.formatter = DateTimeFormatter.ofPattern(pattern);
}
// This adapter is not thread-safe due to mutable state
// Each thread should have its own instance
}public class RobustAdapter extends XmlAdapter<String, CustomType> {
private static final Logger logger = LoggerFactory.getLogger(RobustAdapter.class);
@Override
public CustomType unmarshal(String value) throws Exception {
try {
if (value == null || value.trim().isEmpty()) {
return null;
}
// Conversion logic
CustomType result = parseCustomType(value);
logger.debug("Successfully unmarshalled: {} -> {}", value, result);
return result;
} catch (Exception e) {
logger.error("Failed to unmarshal value: {}", value, e);
throw new IllegalArgumentException("Invalid format for CustomType: " + value, e);
}
}
@Override
public String marshal(CustomType value) throws Exception {
try {
if (value == null) {
return null;
}
String result = formatCustomType(value);
logger.debug("Successfully marshalled: {} -> {}", value, result);
return result;
} catch (Exception e) {
logger.error("Failed to marshal value: {}", value, e);
throw new IllegalStateException("Cannot format CustomType: " + value, e);
}
}
private CustomType parseCustomType(String value) throws Exception {
// Implementation details
}
private String formatCustomType(CustomType value) throws Exception {
// Implementation details
}
}public class NullSafeAdapter extends XmlAdapter<String, Optional<LocalDate>> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
@Override
public Optional<LocalDate> unmarshal(String value) throws Exception {
// Handle null and empty values gracefully
if (value == null || value.trim().isEmpty()) {
return Optional.empty();
}
try {
return Optional.of(LocalDate.parse(value.trim(), FORMATTER));
} catch (DateTimeParseException e) {
// Log warning but don't fail - return empty optional
return Optional.empty();
}
}
@Override
public String marshal(Optional<LocalDate> value) throws Exception {
// Handle Optional container
return value != null && value.isPresent()
? value.get().format(FORMATTER)
: null;
}
}Install with Tessl CLI
npx tessl i tessl/maven-jakarta-xml-bind--jakarta-xml-bind-api