CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-jakarta-xml-bind--jakarta-xml-bind-api

Jakarta XML Binding API that automates the mapping between XML documents and Java objects through data binding

Pending
Overview
Eval results
Files

type-adapters.mddocs/

Type Adapters

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.

Capabilities

XmlAdapter Framework

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;
    }
}

Adapter Registration

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;

Built-in Adapters

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:

  • NormalizedStringAdapter: Normalizes line endings and replaces tabs with spaces
  • CollapsedStringAdapter: Trims leading/trailing whitespace and collapses internal whitespace
  • HexBinaryAdapter: Converts between byte arrays and hexadecimal string representation

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;
}

Common Adapter Patterns

Date and Time Adapters

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;
}

Enum and Complex Type Adapters

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()
        );
    }
}

Collection and Map Adapters

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;
}

Validation and Error Handling

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);
    }
}

Runtime Adapter Management

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);

Best Practices

Thread Safety

// 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
}

Error Handling and Logging

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

Null Handling

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

docs

binary-attachments.md

convenience-api.md

core-binding.md

data-type-conversion.md

index.md

transform-integration.md

type-adapters.md

validation-error-handling.md

xml-mapping-annotations.md

tile.json