JAXB @XmlElements, different types but same name?

19,186

To map this use case you could leverage the following XmlAdapters:

AnimalAdapter

Since AnimalExtension is a super set of Animal we will use it to produce/consume XML. Then we will leverage the value of the animalId property to determine if an instance of Animal or AnimalExtension will be returned to AnimalList.

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

public class AnimalAdapter extends XmlAdapter<AnimalExtension, Animal> {

    @Override
    public Animal unmarshal(AnimalExtension animalExtension) throws Exception {
        if(0 != animalExtension.getAnimalId()) {
            return animalExtension;
        }
        Animal animal = new Animal();
        animal.setName(animalExtension.getName());
        return animal;
    }

    @Override
    public AnimalExtension marshal(Animal animal) throws Exception {
        if(animal.getClass() == AnimalExtension.class) {
            return (AnimalExtension) animal;
        }
        AnimalExtension animalExtension = new AnimalExtension();
        animalExtension.setName(animal.getName());
        return animalExtension;
    }

}

IdAdapter

We will need a second XmlAdapter to suppress animalId if its value is 0:

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

public class IdAdapter extends XmlAdapter<String, Integer> {

    @Override
    public Integer unmarshal(String string) throws Exception {
        return Integer.valueOf(string);
    }

    @Override
    public String marshal(Integer integer) throws Exception {
        if(integer == 0) {
            return null;
        }
        return String.valueOf(integer);
    }

}

Your model classes will be annotated as follows:

AnimalList

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement(name="AnimalList")
public class AnimalList {

    private List<Animal> animalList = new ArrayList<Animal>();

    @XmlElement(name="Animal")
    @XmlJavaTypeAdapter(AnimalAdapter.class)
    public List<Animal> getEntries() {
        return animalList;
    }

}

Animal

import javax.xml.bind.annotation.XmlAttribute;

public class Animal {

    private String name;

    @XmlAttribute
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

AnimalExtension

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

public class AnimalExtension extends Animal {

    private int animalId;

    @XmlAttribute(name="id")
    @XmlJavaTypeAdapter(IdAdapter.class)
    public int getAnimalId() {
        return animalId;
    }

    public void setAnimalId(int animalId) {
        this.animalId = animalId;
    }

}

Demo Code

The following demo code can be used to demonstrate this solution:

import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class Demo {

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

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("input.xml");
        AnimalList animalList = (AnimalList) unmarshaller.unmarshal(xml);

        for(Animal animal : animalList.getEntries()) {
            System.out.println(animal.getClass());
        }

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

}

The following output will be produced:

class AnimalExtension
class Animal
<?xml version="1.0" encoding="UTF-8"?>
<AnimalList xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <Animal name="Don" id="1"/>
   <Animal name="Mike"/>
</AnimalList>

Related Information

You may find the following information useful:

Share:
19,186

Related videos on Youtube

denniss
Author by

denniss

Code

Updated on May 20, 2022

Comments

  • denniss
    denniss almost 2 years

    I have an Animal class and an extension of Animal called AnimalExtension.

    public class Animal
    
    public class AnimalExtension extends Animal
    

    The only difference between the two classes is that AnimalExtension has another instance variable called animalId. Animal does not have this instance variable.

    I also have my own data type that I want to marshal and unamarshal to XML. This data type is called AnimalList. inside AnimalList, there is a list of Animals as an instance variable.

    @XmlType(name = "AnimalList")
    public class AnimalList{
        private List<Animal> animalList;
        ....
    

    animalList can contain both Animal and AnimalExtension. However, on the XML I dont want the element to be named as AnimalExtension; I want them all to have element name of Animal. I only want the extra attribute to show up when JAXB knows that the Animal is actually an instance of AnimalExtension. So if I have a list of that looks like

    List<Animal> animalList = new LinkedList<Animal>();
    AnimalExtension animalExtension = new AnimalExtension();
    animalExtension.setAnimalId(1);
    amimalExtension.setName("Don");
    
    Animal animal = new Animal();
    animal.setName("Mike");
    animalList.add(animalExtension);
    animalList.add(animal);
    

    I want the XML to look like

    <AnimalList>
       <Animal name="Don" id="1" />
       <Animal name="Mike" />
    </AnimalList>
    

    This is what I have tried to do

        @XmlElements(
        {
                @XmlElement(name = "Animal", type = Animal.class),
                @XmlElement(name = "Animal", type = AnimalExtension.class)
            }
        )
        public List<Animal> getEntries() {
            return animalList;
        }
    

    The code compiles but when I try running my server. It gives me this weird error that is so unrelated to what is going on (BeanCreationException). I tried making the name of the XmlElement to be different for each type and that works, but the challenge here is to make the name the same.

    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'encryptionPayloadContentProvider'
    
    • Grzegorz Oledzki
      Grzegorz Oledzki about 13 years
      Given an XML how is JAXB supposed to determine which class to use for Java object creation? Is it existence of id attribute? What if there was other class named AnimalEnhacement with id attribute?