Using JAXB XmlAdapter and XmlJavaTypeAdapter with Interfaces

Lokesh Gupta

The JAXB works with concrete classes. If a class contains fields of type an interface then we get the error “field is an interface, and JAXB can’t handle interfaces“. To solve this error, we can create an XmlAdapter implementation that can handle the conversion between an interface and a concrete class. Ultimately, we must plugin a concrete class in place of the interface in runtime.

1. The Problem with Interface

Suppose we have class BatchJob and it contains a field of Temporal to store the start time of the batch job.

@Data
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement(name = "BatchJob")
@XmlAccessorType(XmlAccessType.FIELD)
class BatchJob {

  Temporal startTime;
}

Let us try to marshal an instance of BatchJob into an XML string using the JAXB Marshaller class.

JAXBContext jaxbContext = JAXBContext.newInstance(BatchJob.class);
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
BatchJob batchJob = new BatchJob(LocalDateTime.now());

StringWriter stringWriter = new StringWriter();
jaxbMarshaller.marshal(batchJob, stringWriter);
String XML = stringWriter.toString();
System.out.println(XML);

This will produce the error in runtime:

Exception in thread "main" org.glassfish.jaxb.runtime.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
java.time.temporal.Temporal is an interface, and JAXB can't handle interfaces.
	this problem is related to the following location:
		at java.time.temporal.Temporal
		at java.time.temporal.Temporal com.howtodoinjava.jaxb.BatchJob.time
		at com.howtodoinjava.jaxb.BatchJob

2. Using XmlAdapter and @XmlJavaTypeAdapter to Provide Concrete Implementation of Interface

As discussed previously, this is necessary for JAXB to have concrete classes. So in this case, we must use an implementation of Temporal interface that will be used in runtime during marshalling and unmarshalling process.

The following TemporalAdapter class extends XmlAdapter. It acts as a bridge between the Temporal interface in the Java object and the string value to be printed in the XML string. We can customize and write our logic to support this conversion. The goal is to provide a mechanism to convert between the Temporal interface and the string value.

class TemporalAdapter extends XmlAdapter<String, Temporal> {

  @Override
  public Temporal unmarshal(String v) throws Exception {
    return LocalDateTime.parse(v);
  }

  @Override
  public String marshal(Temporal v) throws Exception {
    return v.toString();
  }
}

This example assumes that the value in temporal is LocalDateTime representation. If the value is different, use a suitable class.

Next, we annotate the startTime field with @XmlJavaTypeAdapter annotation. This suggests JAXB to use the adapter methods when marshalling and unmarshalling this field.

//...
class BatchJob {

  @XmlJavaTypeAdapter(TemporalAdapter.class)
  Temporal startTime;
}

3. Demo

Let us rerun the program to verify that the adapter works and now we can marshal and unmarshal the BatchJob object to string and vice-versa.

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.io.StringReader;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.time.temporal.Temporal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

public class XmlTypeAdapterOverInterface {

  public static void main(String[] args) throws JAXBException {

    JAXBContext jaxbContext = JAXBContext.newInstance(BatchJob.class);
    Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
    StringWriter stringWriter = new StringWriter();
    jaxbMarshaller.marshal(new BatchJob(LocalDateTime.now()), stringWriter);
    String XML = stringWriter.toString();
    System.out.println(XML);

    Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
    BatchJob newBatchJob = (BatchJob) jaxbUnmarshaller.unmarshal(new StringReader(XML));
    System.out.println(newBatchJob);
  }
}

The program output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><BatchJob><startTime>2024-04-16T18:42:22.944580600</startTime></BatchJob>

BatchJob(startTime=2024-04-16T18:42:22.944580600)

Clearly, we are able to marshal and unmarshal the interface type field using JAXB.

4. Conclusion

In this short JAXB tutorial, we learned to marshal and unmarshal a POJO that has interface-type fields. This helps in preventing the error: “field is an interface, and JAXB can’t handle interfaces”.

Note that for each kind of interface, we must write a custom XmlAdapter class that handles the conversion between the value stored in the interface and its correct representation in the generated XML.

Happy Learning !!

Source Code on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.