31

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!

bdoughan
  • 147,609
  • 23
  • 300
  • 400
Frothy
  • 335
  • 1
  • 3
  • 6

5 Answers5

20

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:

bdoughan
  • 147,609
  • 23
  • 300
  • 400
  • I'm definitely interested in this as a solution, but I can't seem to get it to work. Does it matter that Shape isn't actually my root element? If I try and unmarshall the collection JAXB/MOXy doesn't seem to bother using the Customizer and throws a bunch of errors about trying to instantiate an abstract class. Is there another piece I'm missing? – Frothy Jul 06 '10 at 17:02
  • I believe you are missing the jaxb.properties file that specifies EclipseLink MOXy as the JAXB implementation. I have updated the above example to be more complete. – bdoughan Jul 07 '10 at 14:19
  • You are amazing! This works perfectly. Thank you for going above and beyond to help me with this. – Frothy Jul 08 '10 at 21:08
  • What if the class name is not equal to the type? How can I tell JAXB which type is mapped to which object? – Dikla Feb 22 '16 at 10:56
  • I found https://stackoverflow.com/a/15617571/5987669 describes how to do the same thing, but with annotations. I found it was more intuitive and easier to work with. – Locke Sep 01 '20 at 15:45
8

The annotation @XmlElements enables you to specify which tag corresponds with which subclass.

@XmlElements({
    @XmlElement(name="square", type=Square.class),
    @XmlElement(name="triangle", type=Triangle.class)
})
public List<Shape> getShape() {
    return shape;
}

Also see javadoc for @XmlElements

mileippert
  • 177
  • 8
Barmak
  • 97
  • 1
  • 1
3

AFAIK, you'll have to write an XmlAdapter which knows how to handle the marshal/unmarshalling of the Shape.

Quotidian
  • 2,870
  • 2
  • 24
  • 19
  • `XmlAdapter` is for translating individual String values into a specific type, it's no use for translating complex types. – skaffman Jun 07 '10 at 19:00
  • @Skaffman: XmlAdapter can easily be used with complex types. One of the most common use cases is for mapping java.util.Map to XML. – bdoughan Jul 08 '10 at 15:40
0

No, I'm afraid that's not an option, JAXB isn't that flexible.

The best I can suggest is that you put a method on the Shape class which instantiates the "correct" type based on the attribute. The client code would invoke that factory method to obtain it.

Best I can come up with, sorry.

skaffman
  • 398,947
  • 96
  • 818
  • 769
  • I hadn't thought of this actually. Certainly not ideal but it will probably work. I was really hoping for some way of dropping in a custom adapter or something. Thanks for the quick reply! – Frothy Jun 07 '10 at 19:30
  • @Frothy: JAXB is really quite limited. There are alternatives that give more flexibility, such as JiBX. – skaffman Jun 07 '10 at 19:32
  • I've never heard of JiBX, thanks, I'll look into that this evening. – Frothy Jun 07 '10 at 19:41
  • JAXB is NOT limited, remember it is a specification and implementations such as EclipseLink MOXy offer extensions to easily handle this type of use case (refer to my answer to this question). – bdoughan Jul 08 '10 at 15:38
-1

There is @XmlSeeAlso annotation to tell to bind subclasses.

For example, with the following class definitions:

 class Animal {}
 class Dog extends Animal {}
 class Cat extends Animal {}

The user would be required to create JAXBContext as JAXBContext.newInstance(Dog.class,Cat.class) (Animal will be automatically picked up since Dog and Cat refers to it.)

XmlSeeAlso annotation would allow you to write:

 @XmlSeeAlso({Dog.class,Cat.class})
 class Animal {}
 class Dog extends Animal {}
 class Cat extends Animal {}
Molis
  • 19