May 13, 2011

JAXB and Interface Fronted Models

Usually with JAXB we are mapping POJOs to XML.  In this example I will demonstrate how to handle a Java model that is exposed through a set of interfaces.   This post was inspired by an answer I gave (feel free to upvote) to a question on Stack Overflow.  In the question the poster was running into exceptions like the one below.  This post will cover how to properly map this use case.

Exception in thread "main" com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 6 counts of IllegalAnnotationExceptions
blog.interfaces.Customer is an interface, and JAXB can't handle interfaces.
 this problem is related to the following location:
  at blog.interfaces.Customer
blog.interfaces.Customer does not have a no-arg default constructor.
 this problem is related to the following location:
  at blog.interfaces.Customer
...


Demo Code

When dealing with interface fronted model classes, you interact with the model via the interfaces but you must be sure to bootstrap the JAXBContext on the implementation classes:

package blog.interfaces;

import java.io.File;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(CustomerImpl.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("src/blog/interfaces/input.xml");
        Customer customer = (Customer) unmarshaller.unmarshal(xml);

        Address address = customer.getAddress();
        System.out.println(address.getStreet());

        for(PhoneNumber phoneNumber : customer.getPhoneNumbers()) {
            System.out.println(phoneNumber.getValue());
        }

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(customer, System.out);
    }

}


Interface Model

In this example our domain model is exposed through a set of interfaces.  These interfaces must not be included in the set of classes passed in to bootstrap the JAXBContext.  If they are you will see an exception like the one mentioned earlier in this post.

Customer

package blog.interfaces;

import java.util.List;

public interface Customer {

    Address getAddress();

    void setAddress(Address address);

    List<PhoneNumber> getPhoneNumbers();

    void setPhoneNumbers(List<PhoneNumber> phoneNumbers);

}

Address

package blog.interfaces;

public interface Address {

    public String getStreet();

    public void setStreet(String street);

}

PhoneNumber

package blog.interfaces;

public interface PhoneNumber {

    String getValue();

    void setValue(String value);

}


Implementation Classes

In this example it is the implementation classes that are mapped to XML. 

CustomerImpl

For fields/properties in which the type is an interface we will use the @XmlElement annotation to specify the concrete type.  Note that for collection fields/properties the type specified in @XmlElement represents the item type, and not the collection type.

package blog.interfaces;

import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="customer")
public class CustomerImpl implements Customer {

    private Address address;
    private List phoneNumbers;

    @XmlElement(type=AddressImpl.class)
    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @XmlElement(type=PhoneNumberImpl.class, name="phone-number")
    public List<PhoneNumber> getPhoneNumbers() {
        return phoneNumbers;
    }

    public void setPhoneNumbers(List<PhoneNumber> phoneNumbers) {
        this.phoneNumbers = phoneNumbers;
    }

}

AddressImpl

package blog.interfaces;

public class AddressImpl implements Address {

    private String street;

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

}

PhoneNumberImpl

package blog.interfaces;

import javax.xml.bind.annotation.XmlValue;

public class PhoneNumberImpl implements PhoneNumber {

    private String value;

    @XmlValue
    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

}


XML (input.xml)

The following XML document can be used with this example:

<?xml version="1.0" encoding="UTF-8"?>
<customer>
    <address>
        <street>1 Any Street</street>
    </address>
    <phone-number>555-1111</phone-number>
    <phone-number>555-2222</phone-number>
    <phone-number>555-3333</phone-number>
</customer>


Further Reading

If you enjoyed this post, then you may also be interested in:

4 comments:

  1. Hi,
    This problem is really killing me.
    For marshalling, I don't understand why JAXB needs to be told the concrete class - surely it can work this out using reflection at runtime.

    Annotating with concrete class implementations has 2 problems:
    1) if you have multiple implementations this won't work
    2) the point of programming to interfaces is completely lost

    I've tried looking into using a custom JAXBContext but either providing an enumeration of concrete implementations or the jaxb.index file both seem terrible ways of doing this.

    I can understand the argument for unmarshalling, but this seems bonkers for marshalling.

    ReplyDelete
    Replies
    1. Hi,

      If you are using EclipseLink JAXB (MOXy), I have an approach you can use to make this work. Check out the following example:
      - MOXy JAXB - Map Interfaces to XML

      In that example it is a set of interfaces that are mapped. If you perform an unmarshal you will get back proxy objects. However when you marshal you can pass MOXy any implementation of those interfaces that you want.

      -Blaise

      Delete
    2. I agree. the all point of interface doesn't work

      Delete
    3. Hi Shahar,

      Did you try the approach I recommended in my comment on Jan 31?

      -Blaise

      Delete