Reputation: 1940
I would expect the following test to work with Sun's JAXB RI 2.2.1.1, but instead it fails with a NullPointerException in constructing the JAXBContext:
public class GenericFieldMarshallTest {
public static class CustomType {
}
public static class CustomTypeAdapter extends XmlAdapter<String, CustomType> {
@Override
public String marshal(CustomType v) throws Exception {
return "CustomType";
}
@Override
public CustomType unmarshal(String v) throws Exception {
return new CustomType();
}
}
@XmlJavaTypeAdapter(type = CustomType.class, value = CustomTypeAdapter.class)
public static class RootElement<ValueType> {
@XmlValue public ValueType value;
}
@XmlRootElement(name = "root")
public static class CustomRootElement extends RootElement<CustomType> {
public CustomRootElement() {
value = new CustomType();
}
}
@Test
public void test() throws Exception {
JAXBContext context = JAXBContext.newInstance(CustomRootElement.class,
CustomType.class, RootElement.class);
StringWriter w = new StringWriter();
context.createMarshaller().marshal(new CustomRootElement(), w);
assertThat(w.toString(), equalTo("<root>CustomType</root>"));
}
}
The exception I get is:
java.lang.NullPointerException
at com.sun.xml.bind.v2.runtime.reflect.TransducedAccessor.get(TransducedAccessor.java:165)
at com.sun.xml.bind.v2.runtime.property.ValueProperty.<init>(ValueProperty.java:77)
at com.sun.xml.bind.v2.runtime.property.PropertyFactory.create(PropertyFactory.java:106)
at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.<init>(ClassBeanInfoImpl.java:179)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getOrCreate(JAXBContextImpl.java:515)
at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.<init>(ClassBeanInfoImpl.java:166)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getOrCreate(JAXBContextImpl.java:515)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl.<init>(JAXBContextImpl.java:330)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl$JAXBContextBuilder.build(JAXBContextImpl.java:1140)
at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:154)
at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:121)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:202)
at javax.xml.bind.ContextFinder.find(ContextFinder.java:363)
at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:574)
at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:522)
The cause seems to be that JAXB doesn't know how to marshal the declared type of my field (which I think is erased to Object at runtime?) even though at runtime I only ever set the field to types which JAXB is aware of.
How can I marshal a field whose type is generic?
(Replacing @XmlValue with @XmlAttribute does not fix the exception, nor does changing the declared type of the field to Object, though of course everything works fine if the field is declared as String, except that String is not assignable from CustomType. The placement of @XmlJavaTypeAdapter also makes no difference; in my actual code it is set on the package level in package-info.java.)
Upvotes: 4
Views: 10028
Reputation: 15189
Firstly: Your XmlAdapter
is wrong. The generic types are the other way around.
Then you seem to have to put the @XmlJavaTypeAdapter
on CustomRootElement
.
Furthermore the JAXBContext
needs to be told about all classes involved. Either create a jaxb.index or ObjectFactory
and create the context by giving the package name to the newInstance
method or list all the classes.
The complete code (slightly modified as I use a main()
and not a JUnit test method):
public static class CustomType {
}
public static class CustomTypeAdapter extends
XmlAdapter<String, CustomType> {
@Override
public String marshal(CustomType v) throws Exception {
return "CustomType";
}
@Override
public CustomType unmarshal(String v) throws Exception {
return new CustomType();
}
}
public static class RootElement<V> {
public V value;
}
@XmlJavaTypeAdapter(CustomTypeAdapter.class)
@XmlRootElement(name = "root")
public static class CustomRootElement extends RootElement<CustomType> {
public CustomRootElement() {
value = new CustomType();
}
}
public static void main(String[] args) throws Exception {
JAXBContext context = JAXBContext.newInstance(CustomRootElement.class,
CustomType.class, RootElement.class);
StringWriter w = new StringWriter();
CustomRootElement cre = new CustomRootElement();
cre.value = new CustomType();
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty("jaxb.formatted.output", Boolean.TRUE);
marshaller.marshal(cre, w);
System.err.println(w.toString());
// just to see whether unmarshalling works too
CustomRootElement c = (CustomRootElement) context.createUnmarshaller()
.unmarshal(new StringReader(w.toString()));
System.err.println(c.value);
}
Now the result of marshalling a CustomRootElement
is not what you expect in your test (and neither is it what I expected) but you can unmarshall it and get what you marshalled previously. So (un)marshalling both work but the XML doesn't look as nice:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
<value xsi:type="customType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
</root>
I also put a field in CustomType
and it worked as well. So if you don't need the nice XML this solution should suffice.
I hope I didn't forget any changes I made. If I did, just leave a comment and I'll edit accordingly.
Upvotes: 2