JAXB removing unnecessary nested XML tag

4.7k Views Asked by At

Currently I have following output from my program:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<container>
    <elements>
        <property name="e1">
            <foo name="Alex" status="Married"/>
        </property>
        <property name="e2">
            <foo name="Chris" status="Married with 2 children"/>
        </property>
    </elements>
</container>

As you can see, having both <container> and <elements> tags is useless. I'd like to remove <elements>, so the output would look like:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<container>
    <property name="e1">
        <foo name="Alex" status="Married"/>
    </property>
    <property name="e2">
        <foo name="Chris" status="Married with 2 children"/>
    </property>
</container>

Code that's generating first output is listed below:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Container {

    @XmlElement
    @XmlJavaTypeAdapter(MyAdapter.class)
    private Map<String, Foo> elements = new HashMap<String, Foo>();

    public Container() {
        this.elements = new HashMap<String, Foo>();
    }

    public Map<String, Foo> getElements() {
        return elements;
    }

    @XmlAccessorOrder(XmlAccessOrder.ALPHABETICAL)
    @XmlRootElement(name = "foo")
    static class Foo {
        @XmlAttribute
        public String name;

        @XmlAttribute
        public String status;

        public Foo(String name, String status) {
            this.name = name;
            this.status = status;
        }

        public Foo() {
        }
    }

    public static void main(String[] args) throws JAXBException {
        final JAXBContext context = JAXBContext.newInstance(Container.class);
        final Marshaller m = context.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        final Container c = new Container();
        final Map<String, Foo> elementsMap = c.getElements();
        elementsMap.put("e1", new Foo("Alex", "Married"));
        elementsMap.put("e2", new Foo("Chris", "Married with 2 children"));

        m.marshal(c, System.out);
    }
}

And MyAdapter class, based on JAXB @XmlAdapter: Map -> List adapter? (marshall only) :

public class MyAdapter extends XmlAdapter<MyAdapter.AdaptedFoo, Map<String, Foo>> {

    static class AdaptedFoo {
        public List<Property> property = new ArrayList<>();
    }

    public static class Property {
        @XmlAttribute
        public String name;

        @XmlElementRef(type = Foo.class)
        public Foo value;
    }

    @Override
    public Map<String, Foo> unmarshal(AdaptedFoo v) throws Exception {
        return null;
    }

    @Override
    public AdaptedFoo marshal(Map<String, Foo> map) throws Exception {
        if (null == map) {
            return null;
        }
        AdaptedFoo adaptedFoo = new AdaptedFoo();
        for (Entry<String, Foo> entry : map.entrySet()) {
            Property property = new Property();
            property.name = entry.getKey();
            property.value = entry.getValue();
            adaptedFoo.property.add(property);
        }
        return adaptedFoo;
    }
}

How can I remove <elements> tag from my output?


edit: I've found 'dirty' way to do it - setting @XmlRootElement(name = "") for Container class. But is there any 'elegant' way?

2

There are 2 best solutions below

2
On BEST ANSWER

The XML element <elements> is required to associate the enclosed element seqence with the property Map<?,?> elements. You can't drop it: how would an unmarshaller know where the <property> elements belong *on the level of the element <container>.

Having a List<Property> is different since JAXB handles repeated elements x "hardwired" as a List<?> x, so there's no need for a wrapper.

Since you write Java classes with annotations, you could use this by adding (to Container) another "virtual" field and modify some annotations:

@XmlAccessorType(XmlAccessType.PROPERTY)
public class Container {
// ...
@XmlTransient    
public Map<String,Foo> getElements(){
    return elements;
}
private List<Property> property;
@XMLElement
public List<Property> getProperty(){
    List<Property>  props = elements.entrySet().stream()
                            .map( e -> new Property( e.getKey(), e.getValue() )
                            .collect( Collectors.toList() );
    return props;
}

Older Javas might do

   List<Property> props = new ArrayList<>();
   for( Map.Entry<String,Foo> e: elements.entrySet() ){
       props.add( new Property( e.getKey(), e.getValue() ) );
   }
   return props;

This marshals like this:

<container>
    <property name="aaa">
       <foo name="aaaname" status="aaastatus"/>
    </property>
    <property name="bbb">
       <foo name="bbbname" status="bbbstatus"/>
    </property>
</container>

It doesn't unmarshal!

2
On

Other than removing the tag by "hacking" the element name to be "", I don't think you're going to be able to get around using that data type without the nested tag.

The tag is describing the data type that encompasses your adapted class, which you've annotated as an XML element called elements, which contains properties defined by your adapter.

If you want something that more closely matches what I think you are after, either change the context of your root class, or maybe use a List instead of a HashMap in your container class.