Search code examples
javainterfacejaxbmarshallingunmarshalling

Is it possible for Jaxb to unmarshall to an interface?


my current code marshalls perfectly, and I get the element I want inside of my resulting XML. i.e. <food>Beef</food>

However, the problem comes when I have to unmarshall this back to a java object. Everything returns fine except the food variable. I originally did not have the XmlElement(required = true) on top, and the food element would always unmarshal back to null. Then, I added the required=true section and I am getting issues with the interface. I did some digging and from what I can gather, jaxb can't really unmarshal into an interface since it doesn't know the concrete type to marshall into.

Current error if this helps:

Can not set FoodInterface field BigPayload.food to 
com.sun.org.apache.xerces.internal.dom.ElementNSImpl

My Java classes are as follows:

@XmlSeeAlso({MeatFoods.class, VeggieFoods.class})
@XmlType(name ="BigPayload", propOrder = //stuff goes here
@XmlRootElement(name = foodPayload)
public class BigPayload implements Payload{
    @XmlElements({@XmlElement(type = MeatFoods.class), 
                  @XmlElement(type = VeggieFoods.class),
                  @XmlElement(required = true)})
    protected FoodInterface food;
    protected Grade grade;
    //grade/food setters and getters
}
@XmlTransient //If this isn't here, I get the jaxB cannot handle interfaces and no default constructor error
public interface FoodInterface{ //stuff here}
@XmlType(name = "MeatFoods")
@XmlEnum
public enum MeatFoods implements FoodInterface{
    Chicken(1, Chicken)
    Beef(2, Beef)
    Pork(3, Pork)

    int value;
    String name;

    @Override
    public int getValue()

    @Override
    public String getName()

    public static FoodInterface getEnumFromValue(int value){//gets stuff}
    public static FoodInterface getEnumFromName(String name){//gets stuff}
}

I just wanted to know if that is correct, and there's no real good way to unmarshall an interface type. Is this true? I saw a lot of other questions were about marshalling interfaces, and the unmarshalling questions did not really get answers to my satisfaction. Any answer is appreciated, and I know this isn't a minimal reproducible example, but I'm more looking for a verbal answer instead of a code fix or anything. Although, if there's anything blatantly wrong in the code please let me know!


Solution

  • For the standard cases JAXB can only use (abstract) classes not interfaces.

    Options that i can think of

    • You can use interfaces with @XmlAdapter. See example: [1]
    • Use Object for JAXB Bindings and expose the interface with casting. (Maybe add validation logic into the `afterUnmarshal(Unmarshaller u, Object parent). [2]
    • Bind a private field to @XmlAnyElement and do some further processing in afterUnmarshal(Unmarshaller, Object), add @XmlTransient to the target. See example: [3]

    With some creativity there might be some other options. But i think all boil down to bascially: try to get to the "raw" parsing options and fill the interface reference manually.

    [1]

    public static interface Food {
        String name();
    }
    public enum Veggie implements Food {
        SALAD;
    }
    public static enum Meat implements Food {
        CHICKEN;
    }
    
    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement
    public static class UseInterface {
    
        @XmlJavaTypeAdapter(FoodAdapter.class)
        @XmlAttribute
        private Food food;
    
        public Food getFood() {
            return food;
        }
    
        public void setFood(Food food) {
            this.food = food;
        }
    }
    
    public static class FoodAdapter extends XmlAdapter<String, Food> {
    
        @Override
        public Food unmarshal(String v) throws Exception {
            try {
                return Veggie.valueOf(v);
            } catch (IllegalArgumentException e) {
    
            }
            try {
                return Meat.valueOf(v);
            } catch (IllegalArgumentException e) {
    
            }
            throw new IllegalArgumentException("Unknown Food:" + v);
        }
    
        @Override
        public String marshal(Food v) throws Exception {
            return v.name();
        }
    
    }
    

    [2]

    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement
    public static class UseInterface {
    
        @XmlElement
        private Object food;
    
        public Food getFood() {
            return (Food) food;
        }
    
        public void setFood(Food food) {
            this.food = food;
        }
    
        public void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
            if (food != null && !(food instanceof Food)) {
                throw new IllegalArgumentException("food is of wrong type: " + food.getClass().getName());
            }
        }
    }
    
    JAXBContext newInstance = JAXBContext.newInstance(UseInterface.class, Meat.class, Veggie.class);
    String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><useInterface><food xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"meat\">CHICKEN</food></useInterface>";
    
    newInstance.createUnmarshaller().unmarshal(new StringReader(xml));
    

    [3]

    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement
    public static class UseInterface {
    
        @XmlAnyElement
        private org.w3c.dom.Element foo;
    
        @XmlTransient
        private SomeInterface ifc
    
    
        public void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
            NamedNodeMap attributes = foo.getAttributes();
            // do something with foo on DOM level to bind the subtree to an interface manually
        }
    }