CodeBlue CodeBlue - 4 months ago 11
Java Question

How to make JAXB Marshaller marshal abstract class based on its runtime type?

Consider the following abstract class -

public abstract class Car
{
public abstract void drive(double miles);
}


Here's a sample class (for illustration purposes) that extends the above class.

public class Ferrari extends Car
{
private String lastUsed; // Ferrari specific field not in Car
private boolean f1Car; // Ferrari specific field not in Car

@XmlElement
public void setF1Car(boolean f1Car)
{
this.f1Car = f1Car;
}

public boolean isF1Car() { return f1Car; }

@XmlElement
public void setLastUsed(String lastUsed)
{
this.lastUsed = lastUsed;
}

public String getLastUsed() { return lastUsed; }

public void drive(double miles)
{
// implementation
}
}


I have a report class that contains a Car object -

@XmlRootElement
public class CarTestReport
{
private String date;
private double miles;
private Car car;

@XmlElement
public void setDate(String date) { this.date = date;}

public String getDate() {return date;}

@XmlElement
public void setMiles(double miles) { this.miles = miles; }

public double getMiles() {return miles;}

@XmlElement
public void setCar(Car car) { this.car = car; }

public Car getCar() { return car; }
}


And here is the piece of code using JAXB to Marshall a CarTestReport object -

public static void main(String[] args) throws Exception
{
Ferrari ferrari = new Ferrari();
ferrari.setLastUsed("July 5 2012");
ferrari.setF1Car(false);

CarTestReport report = new CarTestReport();
report.setDate("July 6 2012");
report.setMiles(200);
report.setCar(ferrari);

File file = new File("carTestReport.xml");
JAXBContext jaxbContext = JAXBContext.newInstance(CarTestReport.class);
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();

jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
jaxbMarshaller.marshal(report, file);
}


The problem is, because of the abstract type Car, JAXB ignores it and doesn't marshall the Ferrari object when it marshals the CarTestReport object. The output I get is this -

<carTestReport>
<car/>
<date>July 6 2012</date>
<miles>200.0</miles>
</carTestReport>


As you can see, nothing was entered under the 'car' node, even though the Ferrari object was populated. How to solve this problem?

csd csd
Answer

The JAXB system doesn't look through the classpath for any possible JAXB-annotated classes. You have to help it find them. In your sample code, it simply doesn't know about the existence of the Ferrari class. (It only sees Car because that's the return type of the getter in CarTestReport.)

One quick and dirty way to tell JAXB about Ferrari is to add @XmlSeeAlso({Ferrari.class}) at the top of your Car class. Then you'll get output like this:

<carTestReport>
  <car xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ferrari">
    <f1Car>false</f1Car>
    <lastUsed>July 5 2012</lastUsed>
  </car>
  <date>July 6 2012</date>
  <miles>200.0</miles>
</carTestReport>

Another way to tell JAXB about Ferrari would be to pass that class to the JAXBContext.newInstance method, i.e.:

JAXBContext jaxbContext = JAXBContext.newInstance(CarTestReport.class,
        Ferrari.class);

Or if all of your JAXB classes are in the same package, e.g. com.mycompany.carstuff, then you could do this:

JAXBContext jaxbContext = JAXBContext.newInstance("com.mycompany.carstuff");

And in this last case it WILL search for all classes in that package.

If you want it to emit an element named ferrari (instead of the <car xsi:type="ferrari"> like above), one possibility is to add @XmlType to the top of your Car class, like this:

import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlType;

@XmlSeeAlso({Ferrari.class})
@XmlType
public abstract class Car {
  public abstract void drive(double miles);
}

...and put @XmlRootElement on Ferrari, e.g.:

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Ferrari extends Car {
  // ...
}

From what I understand, this combination of annotations tells JAXB that the Car class maps to an XML schema type (so you won't get any elements named "car"), and that the Ferrari class is an element of that type (so you can have elements named "ferrari"). And the "root" in @XmlRootElement is misleading... it can be an element anywhere in the structure of your objects.