Let XStream call the default constructor where possible

Published by cybso on
This is a post from my original site, which was hosted by the former blog service of the University of Osnabrück. I have moved it to the new site for archiving. Pages linked in this article may no longer work today, and the blog comments under the article no longer exist. Opinions expressed in this article reflect the point of view of the time of publication and do not necessarily reflect my opinion today.

XStream is a nice Java library for serializing and deserializing objects. One of it's advantages is that it does not require the deserialized class to have a default constructor. But sometimes this will be a problem. A simple real-life example:

public class Person {

    public transient final PropertyChangeSupport pcs = new PropertyChangeSupport(this);

    private String name = "";

    public String getName() {
        return name;
    }

    public void setPerson(Name name) {
        pcs.firePropertyChange("name", this.name, this.name = name);
    }
}

The PropertyChangeSupport object should be marked transient, otherwise serializing would include the whole object, including its listeners. But sadly, he following code won't work:

XStream xstream = new XStream();
Person p = new Person();
p.setName("Roland");
String serialized = xstream.toXML(p);
// ...
p = xstream.fromXML(serialized);
System.out.println(p.getName()); // prints "Roland"
p.setName("Cybso"); // Throws NullPointerException in Person.setName(Name)

The call to p.setName("Cybso") throws a NullPointerException because pcs has not been initialized.

There are two standard ways to work around this problem. The first is to initialize XStream using a PureJavaReflectionProvider-Instance:

XStream xstream = new XStream(new PureJavaReflectionProvider());

This would force XStream create new objects using Class.newInstance() - and prevents you from (de)serializing classes without default constructor. The other way is to implement a method called readResolve() which will be called after the object has been created:

public class Person {

    public transient final PropertyChangeSupport pcs;

    private String name;

    public Person() {
        readResolve();
    }

    public void readResolve() {
        pcs = new PropertyChangeSupport(this);
        name = "";
    }

    public String getName() {
        return name;
    }

    public void setPerson(Name name) {
        pcs.firePropertyChange("name", this.name, this.name = name);
    }
}

This means to abandon the using of final transient fields and in this special case it enforces the implementation of a getPCS() or delegation methods. So let me suggest another solution: Create a custom converter that feels responsible for all classes having a default constructor. This reduces the final transient problem to classes without a default constructor.

public static class DefaultConstructorConverter extends ReflectionConverter {
    public DefaultConstructorConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
        super(mapper, reflectionProvider);
    }

    @Override
    public boolean canConvert(Class clazz) {
        for (Constructor c : clazz.getConstructors()) {
            if (c.getParameterTypes().length == 0) {
                return true;
            }
        }
        return false;
    }

    @Override
    protected Object instantiateNewInstance(HierarchicalStreamReader reader, UnmarshallingContext context) {
        try {
            Class clazz = Class.forName(reader.getNodeName());
            return clazz.newInstance();
        } catch (Exception e) {
            throw new ConversionException("Could not create instance of class " + reader.getNodeName(), e);
        }
    }
}

Using this converter the original code will work:

XStream xstream = new XStream();
xstream.registerConverter(new DefaultConstructorConverter(xstream.getMapper(), xstream.getReflectionProvider()));
Person p = new Person();
p.setName("Roland");
String serialized = xstream.toXML(p);
//...
p = xstream.fromXML(serialized);
System.out.println(p.getName()); // prints "Roland"
p.setName("Cybso"); // No exception thrown!
System.out.println(p.getName()); // prints "Cybso"

Happy hacking ;)