Search code examples
javaxmlgenericsxstream

XStream deserialization of generic class returns java.lang.Object


I'm struggling to convert some XML into objects of generic classes. My code works well with non-generic classes, but doesn't with generic ones.

Let's look at this example, it makes it easier to explain. I use both generic and non-generic classes to compare their behaviour.

Xml file:

<?xml version="1.0" encoding="UTF-8"?>
<Box>
    <doubleFieldA>
        <myGenericField>20.3</myGenericField>
    </doubleFieldA>
    <doubleFieldB>
        <myNonGenericField>20.3</myNonGenericField>
    </doubleFieldB>
</Box>

Xsd schema:

<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Box">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="doubleFieldA">
          <xs:complexType>
            <xs:sequence>
              <xs:element type="xs:double" name="myGenericField"/>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
        <xs:element name="doubleFieldB">
          <xs:complexType>
            <xs:sequence>
              <xs:element type="xs:double" name="myNonGenericField"/>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
</xs:element>

GenericClass.java:

public class GenericClass<T> {
    public T myGenericField;
}

NonGenericClass.java:

public class NonGenericClass {
    public double myNonGenericField;
}

Box.java:

import com.thoughtworks.xstream.XStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Box {
    public GenericClass<Double> doubleFieldA;
    public NonGenericClass doubleFieldB;

    private void deserialize() {
        Box b;
        String s;
        XStream xs = new XStream();
        try{
            s = new String(Files.readAllBytes(Paths.get("src/my_xml.xml")));
            b = (Box)xs.fromXML(s);
            doubleFieldA = b.doubleFieldA;
            doubleFieldB = b.doubleFieldB;
        } catch (IOException ioe) {
            System.err.println("IOException: " + ioe.getMessage());
        }
    }

    public Box() {
        deserialize();
    }
}

Main.java:

public class Main {

    public static void main(String[] args) {
        Box box = new Box();
        System.out.println("myGenericField = " + box.doubleFieldA.myGenericField);
        System.out.println("myNonGenericField = " + box.doubleFieldB.myNonGenericField);
    }
}

Output:

myGenericField = java.lang.Object@2d928643

myNonGenericField = 20.3

Expected output (what I want!):

myGenericField = 20.3

myNonGenericField = 20.3

I have a Box class with two attributes: one is a generic class, the other just a normal class.
Both have an attribute, whose type is double in this particular example.
I try to initialize them with the values stored in the XML file (20.3 for both), using XStream.
Eventually, when I print these attributes, I get the correct value for the non-generic class. The generic class attribute, on the contrary, results in a java.lang.Object.
How should I modify my code to obtain the expected behaviour?

I suspect the issue may be in the way XStream handles generic types, but I'm not sure. I don't have much experience with XStream and XML technologies in general, so I'd really appreciate an answer with working code/examples rather than "you need to implement XYZ, find out how in this messy tutorial!".

Thank you very much!

Note: the only purpose of this code is to show my issue. I encountered this problem in a way larger project, so this is just a small example to reproduce it.


Solution

  • There is no good way to do it with XStream, it just doesn't have any idea that generics even exist. You can add custom converter:

    public static class BoxConverter implements Converter {
    
        public boolean canConvert(Class clazz) {
                return clazz.equals(Box.class);
        }
    
        public void marshal(Object value, HierarchicalStreamWriter writer,
                        MarshallingContext context) {
                throw new RuntimeException("to do");
        }
    
        public Object unmarshal(HierarchicalStreamReader reader,
                        UnmarshallingContext context) {
                Box box = new Box();
                 while (reader.hasMoreChildren()) {
                        reader.moveDown();
                        if ("doubleFieldA".equals(reader.getNodeName())) {
                                reader.moveDown();
                                Double val = Double.valueOf(reader.getValue());
                                reader.moveUp();
                                GenericClass<Double> genericObject = new GenericClass<>();
                                genericObject.myGenericField = val;
                                box.doubleFieldA = genericObject;
                        } else if ("doubleFieldB".equals(reader.getNodeName())) {
                                box.doubleFieldB =(NonGenericClass)context.convertAnother(box, NonGenericClass.class);
                        }
                        reader.moveUp();
                }
                return box;
        }
    }
    

    And the register it:

    XStream xs = new XStream();
    xs.registerConverter(new BoxConverter());
    Box b = (Box) xs.fromXML(input);
    

    But that requires to write a separate converter for each class that has GenericClass as a field/member. Notice that when you marshal such object with XStream it generates:

    <Box>
      <doubleFieldA>
        <myGenericField class="double">20.3</myGenericField>
      </doubleFieldA>
      <doubleFieldB>
        <myNonGenericField>20.3</myNonGenericField>
      </doubleFieldB>
    </Box>
    

    and that additional class="double" is something that XStream is unable to infer on its own.