How to use setters in JAXB collections unmarshalling

Tags: , ,



I wan’t to deserialize XML to my POJO but something doing wrong…

My POJO class:

@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement(name="taxi")
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(propOrder = {"id", "name", "phone", "citiesId"})
public class TaxiEntity {
    @Getter @Setter
    private Integer id;

    @Getter @Setter
    private String name;

    @Getter @Setter
    private String phone;

    @Singular("city")
    private Set<Integer> citiesId = new HashSet<>();

    @XmlElementWrapper(name="cities_id")
    @XmlElement(name="city_id")
    public void setCitiesId(Set<Integer> citiesId) {
        System.out.println("setCitiesId()");

        this.citiesId = citiesId;
    }

    public Set<Integer> getCitiesId() {
        System.out.println("getCitiesId()");

        return new HashSet<>(citiesId);
    }
}

Marshalling example:

JAXBContext context = JAXBContext.newInstance(TaxiEntity.class);

    TaxiEntity entity = TaxiEntity.builder().
        id(5).
        name("my city").
        phone("12345678").
             city(1).
             city(5).
    build();
    
            Marshaller marshaller = context.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
            marshaller.marshal(entity, new File("entity.xml"));

XML output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<taxi>
    <id>5</id>
    <name>my city</name>
    <phone>12345678</phone>
    <cities_id>
        <city_id>1</city_id>
        <city_id>5</city_id>
    </cities_id>
</taxi>

Unmarshalling example:

JAXBContext context = JAXBContext.newInstance(TaxiEntity.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
TaxiEntity entity = (TaxiEntity) unmarshaller.unmarshal(new File("entity.xml"));
System.out.println(entity);

Console output:

getCitiesId()
getCitiesId()
TaxiEntity(id=5, name=my city, phone=12345678, citiesId=[])

Process finished with exit code 0

As you can see, citiesId is empty. It happens because JAXB unmarshalling calling the getter (copy of field in my case) and trying to set values into a copy of collection. How to make it create a collection and set it via setter?

P.S. In my real bussiness object, i have collect IDs in getter from DB entities, and cannot return collection in getter.

Thanks!

Answer

—- Edited last time —–

import java.io.File;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

import lombok.Builder;
import lombok.Singular;
import lombok.ToString;

@Builder
@ToString
@XmlRootElement(name = "taxi")
@XmlType(name="taxi", propOrder = { "id", "name", "phone", "citiesId" })
public class TaxiEntity {
    
    private Integer id;
    private String name;
    private String phone;

    @Singular("city")
    private Set<Integer> citiesId;

    public TaxiEntity() {
        
    }
    
    public TaxiEntity(Integer id, String name, String phone, Set<Integer> citiesId) {
        
        System.out.println("Hello");
        this.id = id;
        this.name = name;
        this.phone = phone;
        this.citiesId = citiesId;
    }

    
    @XmlElementWrapper(name = "cities_id")
    @XmlElement(name = "city_id")
    public void setCitiesId(Set<Integer> citiesId) {
        System.out.println("I should be calling during deserialization" + citiesId);
        
        this.citiesId = citiesId;
    }

    @XmlElement
    public void setId(Integer id) {
        this.id = id;
    }
    
    @XmlElement
    public void setName(String name) {
        this.name = name;
    }
    
    @XmlElement
    public void setPhone(String phone) {
        this.phone = phone;
    }

    
    public Integer getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    public String getPhone() {
        return phone;
    }
    
    public Set<Integer> getCitiesId() {
        
        System.out.println("Calling getter " + this.citiesId);
        
        return citiesId;
    }
    
    

    public static void main(String[] args) {

        try {
            JAXBContext context = JAXBContext.newInstance(TaxiEntity.class);
            Marshaller marshaller = context.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);

            Unmarshaller unmarshaller = context.createUnmarshaller();            

            TaxiEntity entity = TaxiEntity.builder().id(5).name("my city").phone("12345678").city(1).city(5).build();

            marshaller.marshal(entity, new File("C:/whee/entity.xml"));
            
            System.out.println("Unmarshalling now ------");
            
            TaxiEntity taxEntityWithSettersGetters = (TaxiEntity) unmarshaller.unmarshal(new File("C:/whee/entity.xml"));
            
            System.out.println(taxEntityWithSettersGetters);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Printout:

Hello
Calling getter [1, 5]
Unmarshalling now ------
Calling getter null
I should be calling during deserialization[]
Calling getter [1, 5]
TaxiEntity(id=5, name=my city, phone=12345678, citiesId=[1, 5])

During unmarshalling JAXB checks if your collection is null, if it is (It will call the setter for the first time to initialize it to empty), and you can see that in the log.

However, afterwards, it will use its internal logic to populate the collection (SET), initialize its type (New Set)*by using the Setter you have, and use the Set.add(xyz); to add (1), then (5).

The JAXB Logic invoked is found in class:

public abstract class Lister<BeanT,PropT,ItemT,PackT> {

//startPacking is calling to initialize the collection Set so it is empty

public T startPacking(BeanT bean, Accessor<BeanT, T> acc) throws AccessorException {
    T collection = acc.get(bean);
    if(collection==null) {
        collection = ClassFactory.create(implClass);
        if(!acc.isAdapted())
            acc.set(bean,collection);
    }
    collection.clear();
    return collection;
}

//Right way, this gets called afterwards (Before any of your TaxiEntity logic), to do addToPack(1), addToPack(5), <— Now your Set has [1,5]

public void addToPack(T collection, Object o) {
    collection.add(o);
}

Then, you see in the log, it calls getCitiesIds(), and you will see magically it has [1,5]

Its the way JAXB works with Collections. All other elements, their proper Setters are called.

See, JAXB does not call Setter method

You need to think of a different way of doing it, rather than dependending on the getter/setter. It did its job of unmarshalling the object from the XML file, the rest of the logic could be written in an external method.



Source: stackoverflow