1
votes

I have a situation in which a Java object contains a generic Payload<T> that needs to be marshalled into xml. So given the following classes:

AbstractBox

@XmlTransient
public abstract class AbstractBox {
    String type = this.getClass().getSimpleName();
    String name = this.getClass().getCanonicalName();

    // setters/getters snipped

    public abstract String saySomething();
}

SmallBox

@XmlRootElement(name="small-box")
@XmlType(name="small-box")
public class SmallBox extends AbstractBox {

    @XmlElement
    public String getMsg() {
        return saySomething();
    }
    public void setMsg(String msg) {
        // do nothing
    }

    @Override
    public String saySomething() {
        return "I'm a small box";
    }
}

Payload

@XmlTransient
public class Payload<T> {
    private T payload;

    public T getPayload() {
        return payload;
    }

    public void setPayload(T payload) {
        this.payload = payload;
    }
}

and some code like this:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class JaxbAnnotationsTest<P>
{
    String name;
    int age;
    int id;
    Payload<P> payload;

    // setters and getters snipped

    public static void main(String[] args) {
        JaxbAnnotationsTest<AbstractBox> example = new JaxbAnnotationsTest<AbstractBox>();
        example.setName("Brion");
        example.setId(100);
        example.setAge(34);
        Payload<AbstractBox> object = new Payload<AbstractBox>();
        object.setPayload(new SmallBox());
        example.setPayloadContainer(object);

        try {
            XmlMapper xmapper = new XmlMapper();
            xmapper.writeValue(System.out, example);
        } catch (Exception ex) {
            System.err.println("Damn..." + ex.getMessage());
        }
    }

I expect to see:

<JaxbAnnotationsTest>
  <name>Brion</name>
  <age>34</age>
  <id>100</id>
  <payloadContainer>
    <small-box>
      <type>SmallBox</type>
      <name>sandbox.xml.jaxb.annotations.SmallBox</name>
      <msg>I'm a small box</msg>
    </small-box>
  </payloadContainer>
</JaxbAnnotationsTest>

but instead I get:

<JaxbAnnotationsTest>
  <name>Brion</name>
  <age>34</age>
  <id>100</id>
  <payloadContainer>
    <payload>
      <type>SmallBox</type>
      <name>sandbox.xml.jaxb.annotations.SmallBox</name>
      <msg>I'm a small box</msg>
    </payload>
  </payloadContainer>
</JaxbAnnotationsTest>

I've tried using @XmlType on the concrete subclass to change payload to small-box but that didn't work either. If I remove the Payload<P> object and simply have a class member payload of generic type P then the paymentContainer tag goes away, but payload remains and does not use the small-box name I've specified.

Is there a way for me to force JAXB (any implementation) to set the tag to the name specified in the subclass instead of the generic type property?

Update: The selected answer provides the solution but I wanted to follow up in the question as well. My problem was two-fold:

  1. I was using @XmlTransient on the Payload<T> class and needed to instead use an @XmlAnyElement annotation on the setPayload(T payload) method (though I suspect it doesn't matter which method of the setter/getter pair is annotated as long as only one has the annotation).
  2. I was using Jackson 2's JacksonJaxbXmlProvider which is an incomplete JAXB implementation that was ignoring the @XmlRootElement of the element used as the value of the @XmlAnyElement-annotated property.

Changing my JAXB provider to use the Java 6/7 built-in JAXB provider generated the output I expected.

1

1 Answers

2
votes

Your Payload class expects any bean type as a property, so JAXB doesn't know how to marshall that particular object (in this case SmallBox). Since you need to keep the generic property in Payload the solution should be

  1. Remove @XmlTransient annotation to make these types available for marshalling (I am wondering how it worked with this annotation as you mentioned)
  2. Annotate setPayload in Payload class with @XmlAnyElement as follows

public class Payload { private T payload;

public T getPayload() {
    return payload;
}

@XmlAnyElement
public void setPayload(T payload) {
    this.payload = payload;
}

}

@XmlAnyElement javadoc says

Maps a JavaBean property to XML infoset representation and/or JAXB element.

This means any known bean type (annotated with @XmlRootElement) which is passed into setPayload() will be resolved by the JAXB to their corresponding type, here that bean is SmallBox, otherwise to a default element type ( i think it should be the default implementation of org.w3c.dom.Element). After this change it will marshall the JaxbAnnotationsTest nicely to following xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<jaxbAnnotationsTest>
    <age>34</age>
    <id>100</id>
    <name>Brion</name>
    <payloadContainer>
        <small-box>
            <name>org.mycode.SmallBox</name>
            <type>SmallBox</type>
            <msg>I'm a small box</msg>
        </small-box>
    </payloadContainer>
</jaxbAnnotationsTest>

Hope that will help you.