Let XStream call the default constructor where possible

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:

  1. public class Person {
  2.  
  3. public transient final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
  4.  
  5. private String name = "";
  6.  
  7. public String getName() {
  8. return name;
  9. }
  10.  
  11. public void setPerson(Name name) {
  12. pcs.firePropertyChange("name", this.name, this.name = name);
  13. }
  14. }

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

  1. XStream xstream = new XStream();
  2. Person p = new Person();
  3. p.setName("Roland");
  4. String serialized = xstream.toXML(p);
  5. // ...
  6. p = xstream.fromXML(serialized);
  7. System.out.println(p.getName()); // prints "Roland"
  8. 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 ;)

Comments

Thanks for this post, surprisingly it's not much around on the web about this topic.
Unfortunately this seems only to work if there are no aliases defined for class names...

You can ask XStream to resolve the mapping between alias and Class, instead of calling Class.forName():

Replace Class.forName(reader.getNodeName()) with mapper.realClass(reader.getNodeName())

I found this blogpost, since i had the exact same problem with a transient PropertyChangeSupport. I decided to go with the readResolve() solution.

There is a small error in your example code, that took me some extra minutes to resolve by looking at

http://xstream.codehaus.org/faq.html

The readResolve() method needs to return it's own type. Like this:

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

This worked for me.

Add new comment