Search code examples
javainheritancejaxbeclipselink

Java/JAXB: Unmarshall Xml to specific subclass based on an attribute


Is it possible to use JAXB to unmarshall xml to a specific Java class based on an attribute of the xml?

<shapes>
  <shape type="square" points="4" square-specific-attribute="foo" />
  <shape type="triangle" points="3" triangle-specific-attribute="bar" />
</shapes>

I would like to have a List of Shape objects containing a triangle and a square, each with their own shape-specific attribute. IE:

abstract class Shape {
    int points;
    //...etc
}

class Square extends Shape {
    String square-specific-attribute;
    //...etc
}

class Triangle extends Shape {
    String triangle-specific-attribute;
    //...etc
}

I'm currently just putting all attributes in one big "Shape" class and it's less than ideal.

I could get this to work if the shapes were properly named xml elements, but unfortunately I don't have control of the xml I'm retrieving.

Thanks!


Solution

  • JAXB is a spec, specific implementations will provide extension points to do things such as this. If you are using EclipseLink JAXB (MOXy) you could modify the Shape class as follows:

    import javax.xml.bind.annotation.XmlAttribute;
    import org.eclipse.persistence.oxm.annotations.XmlCustomizer;
    
    @XmlCustomizer(ShapeCustomizer.class)
    public abstract class Shape {
    
        int points;
    
        @XmlAttribute
        public int getPoints() {
            return points;
        }
    
        public void setPoints(int points) {
            this.points = points;
        }
    
    }
    

    Then using the MOXy @XMLCustomizer you could access the InheritancePolicy and change the class indicator field from "@xsi:type" to just "type":

    import org.eclipse.persistence.config.DescriptorCustomizer;
    import org.eclipse.persistence.descriptors.ClassDescriptor;
    
    public class ShapeCustomizer implements DescriptorCustomizer {
    
        @Override
        public void customize(ClassDescriptor descriptor) throws Exception {
            descriptor.getInheritancePolicy().setClassIndicatorFieldName("@type");
        }
    }
    

    You will need to ensure that you have a jaxb.properties file in with you model classes (Shape, Square, etc) with the following entry specifying the EclipseLink MOXy JAXB implementation:

    javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
    

    Below is the rest of the model classes:

    Shapes

    import java.util.ArrayList;
    import java.util.List;
    
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    public class Shapes {
    
        private List<Shape> shape = new ArrayList<Shape>();;
    
        public List<Shape> getShape() {
            return shape;
        }
    
        public void setShape(List<Shape> shape) {
            this.shape = shape;
        }
    
    }
    

    Square

    import javax.xml.bind.annotation.XmlAttribute;
    
    public class Square extends Shape {
        private String squareSpecificAttribute;
    
        @XmlAttribute(name="square-specific-attribute")
        public String getSquareSpecificAttribute() {
            return squareSpecificAttribute;
        }
    
        public void setSquareSpecificAttribute(String s) {
            this.squareSpecificAttribute = s;
        }
    
    }
    

    Triangle

    import javax.xml.bind.annotation.XmlAttribute;
    
    public class Triangle extends Shape {
        private String triangleSpecificAttribute;
    
        @XmlAttribute(name="triangle-specific-attribute")
        public String getTriangleSpecificAttribute() {
            return triangleSpecificAttribute;
        }
    
        public void setTriangleSpecificAttribute(String t) {
            this.triangleSpecificAttribute = t;
        }
    
    }
    

    Below is a demo program to check that everything works:

    import java.io.StringReader;
    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 jaxbContext = JAXBContext.newInstance(Shapes.class, Triangle.class, Square.class);
    
            StringReader xml = new StringReader("<shapes><shape square-specific-attribute='square stuff' type='square'><points>4</points></shape><shape triangle-specific-attribute='triangle stuff' type='triangle'><points>3</points></shape></shapes>");
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            Shapes root = (Shapes) unmarshaller.unmarshal(xml);
    
            Marshaller marshaller = jaxbContext.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            marshaller.marshal(root, System.out);
        }
    }
    

    I hope this helps.

    For more information on EclipseLink MOXy see:

    EDIT

    In EclipseLink 2.2 we're making this easier to configure, check out the following article for more information: