Search code examples
javaandroidxstream

XStream serialize custom Map with NamedMapConverter


I am successfully serializing my Base class containing HashMap<String, String> with NamedMapConverter. My requirements have changed and I need to change HashMap to custom StringDictionary class that extends HashMap<String, String>.

But after using StringDictionary, my serialization got broken. Is there a way to configure XStream and/or NamedMapConverter to preserve needed xml output without writing custom converters?

Using XStream 1.4.7 on Android.


Required xml output:

<base>
  <values>
    <val key="123">111</val>
    <val key="abc">aaa</val>
  </values>
</base>

Base class with HashMap and XStream configuration that produces above output

public class Base
{
    public Map<String, String> values = new HashMap<>();
}


Base base = new Base();
base.values.put("abc", "aaa");
base.values.put("123", "111");

XStream xs = new XStream();
xs.alias("base", Base.class);
NamedMapConverter c = new NamedMapConverter(xs.getMapper(), "val", "key", String.class, null, String.class, true, false, xs.getConverterLookup());
xs.registerConverter(c);

String s = xs.toXML(base);

Using modified Base class

public class Base
{
    public Map<String, String> values = new StringDictionary();
}

produces following incorrect xml output:

<base>
  <values class="com.test.StringDictionary" serialization="custom">
    <unserializable-parents/>
    <map>
      <default>
        <loadFactor>0.75</loadFactor>
      </default>
      <int>4</int>
      <int>2</int>
      <string>123</string>
      <string>111</string>
      <string>abc</string>
      <string>aaa</string>
    </map>
  </values>
</base>

Solution

  • It seems that without custom converter above is impossible to achieve. However, in this case custom converter can be provided with only slight tweaking of existing one. Problem lies in NamedMapConverter.canConvert method that has to be overridden to include custom StringDictionary class.

    Following code will produce required xml output for modified Base class:

    XStream xs = new XStream();
    xs.alias("base", Base.class);
    
    xs.addDefaultImplementation(StringDictionary.class, Map.class);
    NamedMapConverter c = new NamedMapConverter(xs.getMapper(), "val", "key", String.class, null, String.class, true, false, xs.getConverterLookup())
    {
        public boolean canConvert(Class type)
        {
            if (type.equals(StringDictionary.class)) return true;
            else return super.canConvert(type);
        }
    };
    xs.registerConverter(c);
    

    Another simpler (but not fully compliant) solution, is to pass StringDictionary class to NamedMapConverter constructor. However this solution will slightly change xml output.

    XStream xs = new XStream();
    xs.alias("base", Base.class);
    NamedMapConverter c = new NamedMapConverter(StringDictionary.class, xs.getMapper(), "val", "key", String.class, null, String.class, true, false, xs.getConverterLookup());
    xs.registerConverter(c);
    
    
    <base>
      <values class="com.test.StringDictionary">
        <val key="123">111</val>
        <val key="abc">aaa</val>
      </values>
    </base>