views:

322

answers:

1

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.)

+1  A: 

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"/&gt;
</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.

musiKk
You're right about the order of the XmlAdapter's types, I intended for them to be the other way around. This was a transcription error when I wrote up the fake example for my question. I've fixed this in my question.However none of the rest of your suggestions result in working code for me.The placement of the @XmlJavaTypeAdapter annotation is arbitrary; in my actual code, it is at the package level in package-info.class.I've tried adding the other classes involved to the JAXBContext.newInstance() call in case JAXB can't find them itself, but that also makes no difference to the problem.
DanC
If you have a working version of the test in my question, can you please put it in a pastebin and link to it here, so we can see what you changed in order to get it working?
DanC
I edited the post. Don't need to bring external dependencies like pastebin into this. I hope this works for you because I used JAXB as it is distributed with the JDK.
musiKk
Thanks for updating your answer. Unfortunately you've left out the @XmlValue annotation on the field of RootElement. When I add that back in (since I need the field's value to become the content of the XML element) I see the original exception I pasted. I suppose this means either I am misusing @XmlValue or there is a bug with Sun's JAXB connected to @XmlValue somehow.
DanC
Well, to be honest I never used @XmlValue and after reading the documentation I don't really see why you would need it here. If it really is necessary, I don't think I can be of further assistance, sorry.
musiKk