How can JAXB XmlAdapter be used to marshall lists?

Steve picture Steve · Jan 28, 2013 · Viewed 28.5k times · Source

Instances of this class are part of a large object graph and are not at the root of the object graph:

public class Day
{
    public Day(LocalDate date, List<LocalTime> times)
    {
        this.date = date;
        this.times = times;
    }

    public Day()
    {
        this(null, null);
    }

    public LocalDate getDate()
    {
        return date;
    }

    public List<LocalTime> getTimes()
    {
        return times;
    }

    private final LocalDate date;

    private final List<LocalTime> times;
}

The object graph is converted to JSON using Jersey and JAXB. I have XmlAdapters registered for LocalDate and LocalTime.

The problem is that it's only working for the date property and not the times property. I suspect this has something to do with the fact that times is a list rather than a single value. How, then, do I tell Jersey/JAXB to marshall each element in the times list using the registered XmlAdapter?

Update:

I confirmed that LocalTime marshalling is indeed working for scalar LocalTime properties by adding a scalar LocalTime property and observing the expected output in the JSON.

For completeness, here's package-info.java:

@XmlJavaTypeAdapters({
    @XmlJavaTypeAdapter(value = LocalDateAdapter.class, type = LocalDate.class),
    @XmlJavaTypeAdapter(value = LocalTimeAdapter.class, type = LocalTime.class)
})
package same.package.as.everything.else;

LocalDateAdapter:

public class LocalDateAdapter extends XmlAdapter<String, LocalDate>
{
    @Override
    public LocalDate unmarshal(String v) throws Exception
    {
        return formatter.parseLocalDate(v);
    }

    @Override
    public String marshal(LocalDate v) throws Exception
    {
        return formatter.print(v);
    }

    private final DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyyMMdd");
}

LocalTimeAdapter:

public class LocalTimeAdapter extends XmlAdapter<String, LocalTime>
{
    @Override
    public LocalTime unmarshal(String v) throws Exception
    {
        return formatter.parseLocalTime(v);
    }

    @Override
    public String marshal(LocalTime v) throws Exception
    {
        return formatter.print(v);
    }

    private final DateTimeFormatter formatter = DateTimeFormat.forPattern("HHmm");

Answer

bdoughan picture bdoughan · Jan 29, 2013

An XmlAdapter for a class is applied to a mapped field/property of that type, and in the case of collections, for each item in the collection. The example below proves that this works. Have you tried running your example standalone to XML to verify the mappings that way. Is suspect the problem is something else other than the XmlAdapter specifically.

StringAdapter

The following XmlAdapter will convert a String to lower case on the unmarshal operation and convert it to upper case when marshalled.

package forum14569293;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class StringAdapter extends XmlAdapter<String, String> {

    @Override
    public String unmarshal(String v) throws Exception {
        return v.toLowerCase();
    }

    @Override
    public String marshal(String v) throws Exception {
        return v.toUpperCase();
    }

}

package-info

Just as in your question a package level @XmlJavaTypeAdapters annotation will be used to register the XmlAdapter. This will register this XmlAdapter for all mapped String properties within this package (see: http://blog.bdoughan.com/2012/02/jaxb-and-package-level-xmladapters.html).

@XmlJavaTypeAdapters({
    @XmlJavaTypeAdapter(value=StringAdapter.class, type=String.class)
})
package forum14569293;

import javax.xml.bind.annotation.adapters.*;

Root

Below is a sample domain model similar to your Day class with two mapped properties. The first is of type String and the second List<String>. One thing I notice about your Day class is that you only have get methods. This means that you will need to add an @XmlElement annotation for a JAXB impl to consider that a mapped property.

package forum14569293;

import java.util.*;
import javax.xml.bind.annotation.*;

@XmlRootElement
public class Root {

    public Root(String foo, List<String> bar) {
        this.foo = foo;
        this.bar = bar;
    }

    public Root() {
        this(null, null);
    }

    @XmlElement
    public String getFoo() {
        return foo;
    }

    @XmlElement
    public List<String> getBar() {
        return bar;
    }

    private final String foo;
    private final List<String> bar;

}

Demo

package forum14569293;

import java.util.*;
import javax.xml.bind.*;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Root.class);

        List<String> bar = new ArrayList<String>(3);
        bar.add("a");
        bar.add("b");
        bar.add("c");
        Root root = new Root("Hello World", bar);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(root, System.out);

    }

}

Output

Below is the output from running the demo code we see that all the strings were converted to upper case by the XmlAdapter.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <bar>A</bar>
    <bar>B</bar>
    <bar>C</bar>
    <foo>HELLO WORLD</foo>
</root>

UPDATE

Thanks. I tried it and the XML consisted of one empty tag only, meaning there's something about the POJO model that JAXB doesn't like. (Perhaps it should be Serializable?)

JAXB does not require that POJOs implement Serializable.

That's interesting because it seems to indicate that the only part JAXB plays in this is to lend its annotations and some other interfaces (e.g. XmlAdapter) to the JSON (de)serializer and that's where the relationship ends.

It depends on what is being used as the JSON binding layer. The JAXB (JSR-222) specification does not cover JSON-binding so this type of support is beyond the spec. EclipseLink JAXB (MOXy) offers native JSON-binding (I'm the MOXy lead) with it you could do something like:

Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
marshaller.marshal(root, System.out);

And get the following output that takes the XmlAdapter into account:

{
   "bar" : [ "A", "B", "C" ],
   "foo" : "HELLO WORLD"
}

Nevertheless, when I get an opportunity I will do what I can to get JAXB to generate the XML and that may reveal something else.

This would be useful as I do not believe what you are seeing is a JAXB issue, but an issue in the JSON binding layer that you are using.