I need to serialize / deserialize a Map<String, Object> to / from XML with 
dataformat-xml / XmlMapper.

It works fine with JSON when using default typing.
But when deserialize the XML, the typing of primitive types (especially 
numeric and boolean) get lost.

Every number and boolean is treated as string, when deserializing.

java.lang.AssertionError: expected: java.lang.Integer<123> but was: java.
lang.String<123>

At the moment I manage with my own module and serializers for the primitive 
types.
This works fine only with JsonTypeInfo.As.WRAPPER_OBJECT, but with 
JsonTypeInfo.As.WRAPPER_ARRAY, deserialization fails.
Attached is a unit test and the module.

But I wonder if there is not a better way?

Best regards
Tristan

-- 
You received this message because you are subscribed to the Google Groups 
"jackson-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to jackson-user+unsubscr...@googlegroups.com.
To post to this group, send email to jackson-user@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;

import java.io.IOException;

public class CompletelyTypedJacksonModule extends SimpleModule {
    public CompletelyTypedJacksonModule() {
        addSerializer(String.class, new TypedStringSerializer());
        addSerializer(Boolean.class, new TypedBooleanSerializer());
        addSerializer(Short.class, new TypedShortSerializer());
        addSerializer(Integer.class, new TypedIntegerSerializer());
        addSerializer(Float.class, new TypedFloatSerializer());
        addSerializer(Double.class, new TypedDoubleSerializer());
    }

    public static class TypedStringSerializer extends StdScalarSerializer<String> {
        public TypedStringSerializer() {
            super(String.class, false);
        }

        @Override
        public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            if (null == value) {
                gen.writeNull();
            } else {
                gen.writeString(value);
            }
        }
    }

    public static class TypedBooleanSerializer extends StdScalarSerializer<Boolean> {
        public TypedBooleanSerializer() {
            super(Boolean.class, false);
        }

        @Override
        public void serialize(Boolean value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            if (null == value) {
                gen.writeNull();
            } else {
                gen.writeBoolean(value);
            }
        }
    }

    public static class TypedByteSerializer extends StdScalarSerializer<Byte> {
        public TypedByteSerializer() {
            super(Byte.class, false);
        }

        @Override
        public void serialize(Byte value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            if (null == value) {
                gen.writeNull();
            } else {
                gen.writeNumber(value);
            }
        }
    }

    public static class TypedShortSerializer extends StdScalarSerializer<Short> {
        public TypedShortSerializer() {
            super(Short.class, false);
        }

        @Override
        public void serialize(Short value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            if (null == value) {
                gen.writeNull();
            } else {
                gen.writeNumber(value);
            }
        }
    }

    public static class TypedIntegerSerializer extends StdScalarSerializer<Integer> {
        public TypedIntegerSerializer() {
            super(Integer.class, false);
        }

        @Override
        public void serialize(Integer value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            if (null == value) {
                gen.writeNull();
            } else {
                gen.writeNumber(value);
            }
        }
    }

    public static class TypedFloatSerializer extends StdScalarSerializer<Float> {
        public TypedFloatSerializer() {
            super(Float.class, false);
        }

        @Override
        public void serialize(Float value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            if (null == value) {
                gen.writeNull();
            } else {
                gen.writeNumber(value);
            }
        }
    }

    public static class TypedDoubleSerializer extends StdScalarSerializer<Double> {
        public TypedDoubleSerializer() {
            super(Double.class, false);
        }

        @Override
        public void serialize(Double value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            if (null == value) {
                gen.writeNull();
            } else {
                gen.writeNumber(value);
            }
        }
    }

}
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import static org.junit.Assert.assertEquals;

@RunWith(Parameterized.class)
public class XmlMapperTest {

    public static final String KEY_STRING = "s";
    public static final String KEY_INTEGER = "i";
    public static final String KEY_LONG = "l";
    public static final String KEY_FLOAT = "f";
    public static final String KEY_DOUBLE = "d";
    public static final String KEY_BIG_INTEGER = "bi";
    public static final String KEY_BIG_DECIMAL = "bd";
    public static final String KEY_BOOLEAN = "b";

    @Parameterized.Parameters(name = "{1} <=> {2}")
    public static Collection<Object[]> data() throws IOException {
        Map<String, Object> expectedObject = new LinkedHashMap<>();
        expectedObject.put(KEY_STRING, "Hello world");
        expectedObject.put(KEY_INTEGER, 123);
        expectedObject.put(KEY_LONG, 456L);
        expectedObject.put(KEY_FLOAT, 1.2f);
        expectedObject.put(KEY_DOUBLE, 3.4d);
        expectedObject.put(KEY_BIG_INTEGER, new BigInteger("159"));
        expectedObject.put(KEY_BIG_DECIMAL, new BigDecimal("1.5"));
        expectedObject.put(KEY_BOOLEAN, true);

        TypeReference typeReference = new TypeReference<LinkedHashMap<String, Object>>() {
        };

        // Case 1: default typing as wrapper array
        XmlMapper xmlMapperWrapperArray = new XmlMapper();
        xmlMapperWrapperArray.findAndRegisterModules();
        xmlMapperWrapperArray.registerModule(new CompletelyTypedJacksonModule());
        xmlMapperWrapperArray.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS, JsonTypeInfo.As.WRAPPER_ARRAY);

        String expectedXmlWrapperArray = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n" +
            "    <LinkedHashMap>\n" +
            "        <s>java.lang.String</s>\n" +
            "        <s>Hello world</s>\n" +
            "        <i>java.lang.Integer</i>\n" +
            "        <i>123</i>\n" +
            "        <l>java.lang.Long</l>\n" +
            "        <l>456</l>\n" +
            "        <f>java.lang.Float</f>\n" +
            "        <f>1.2</f>\n" +
            "        <d>java.lang.Double</d>\n" +
            "        <d>3.4</d>\n" +
            "        <bi>java.math.BigInteger</bi>\n" +
            "        <bi>159</bi>\n" +
            "        <bd>java.math.BigDecimal</bd>\n" +
            "        <bd>1.5</bd>\n" +
            "        <b>java.lang.Boolean</b>\n" +
            "        <b>true</b>\n" +
            "    </LinkedHashMap>\n" +
            "</root>\n";

        // Case 2: default typing as wrapper object
        XmlMapper xmlMapperWrapperObject = new XmlMapper();
        xmlMapperWrapperObject.findAndRegisterModules();
        xmlMapperWrapperObject.registerModule(new CompletelyTypedJacksonModule());
        xmlMapperWrapperObject.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS, JsonTypeInfo.As.WRAPPER_OBJECT);

        // Case 2/1:
        String expectedXmlWrapperObject = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>\n" +
            "    <LinkedHashMap>\n" +
            "        <s>\n" +
            "            <java.lang.String>Hello world</java.lang.String>\n" +
            "        </s>\n" +
            "        <i>\n" +
            "            <java.lang.Integer>123</java.lang.Integer>\n" +
            "        </i>\n" +
            "        <l>\n" +
            "            <java.lang.Long>456</java.lang.Long>\n" +
            "        </l>\n" +
            "        <f>\n" +
            "            <java.lang.Float>1.2</java.lang.Float>\n" +
            "        </f>\n" +
            "        <d>\n" +
            "            <java.lang.Double>3.4</java.lang.Double>\n" +
            "        </d>\n" +
            "        <bi>\n" +
            "            <java.math.BigInteger>159</java.math.BigInteger>\n" +
            "        </bi>\n" +
            "        <bd>\n" +
            "            <java.math.BigDecimal>1.5</java.math.BigDecimal>\n" +
            "        </bd>\n" +
            "        <b>\n" +
            "            <java.lang.Boolean>true</java.lang.Boolean>\n" +
            "        </b>\n" +
            "    </LinkedHashMap>\n" +
            "</root>\n";

        return Arrays.asList(new Object[][]{
            {
                xmlMapperWrapperArray,
                expectedObject,
                expectedXmlWrapperArray,
                typeReference
            },
            {
                xmlMapperWrapperObject,
                expectedObject,
                expectedXmlWrapperObject,
                typeReference
            }
        });
    }

    private final XmlMapper xmlMapper;
    private final Map<String, Object> expectedObject;
    private final String expectedXml;
    private final TypeReference<LinkedHashMap<String, Object>> typeReference;

    public XmlMapperTest(XmlMapper xmlMapper, Map<String, Object> expectedObject, String expectedXml, TypeReference<LinkedHashMap<String, Object>> typeReference) {
        this.xmlMapper = xmlMapper;
        this.expectedObject = expectedObject;
        this.expectedXml = expectedXml;
        this.typeReference = typeReference;
    }

    @Test
    public void testSerialization() throws Exception {
        final Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        document.setXmlStandalone(true);

        final Element node = document.createElement("root");
        document.appendChild(node);

        final Result result = new DOMResult(node);
        final XMLStreamWriter xmlStreamWriter = XMLOutputFactory.newFactory().createXMLStreamWriter(result);
        xmlMapper.writeValue(xmlStreamWriter, expectedObject);

        String actualXml = toXML(document);
        assertEquals(expectedXml, actualXml);
    }

    @Test
    public void testDeserializationWithClass() throws Exception {
        final Document document = fromXML(expectedXml);
        final Element documentElement = document.getDocumentElement();
        final Element element = getChildElement(documentElement);
        final DOMSource source = new DOMSource(element);
        final XMLStreamReader xmlStreamReader = XMLInputFactory.newFactory().createXMLStreamReader(source);
        final LinkedHashMap actualObject = xmlMapper.readValue(xmlStreamReader, LinkedHashMap.class);
        // final LinkedHashMap actualObject = xmlMapper.readValue(expectedXml, LinkedHashMap.class);

        assertEquals(expectedObject.keySet(), actualObject.keySet());
        assertEquals(expectedObject.get(KEY_STRING), actualObject.get(KEY_STRING));
        assertEquals(expectedObject.get(KEY_INTEGER), actualObject.get(KEY_INTEGER));
        assertEquals(expectedObject.get(KEY_LONG), actualObject.get(KEY_LONG));
        assertEquals(expectedObject.get(KEY_FLOAT), actualObject.get(KEY_FLOAT));
        assertEquals(expectedObject.get(KEY_DOUBLE), actualObject.get(KEY_DOUBLE));
        assertEquals(expectedObject.get(KEY_BIG_INTEGER), actualObject.get(KEY_BIG_INTEGER));
        assertEquals(expectedObject.get(KEY_BIG_DECIMAL), actualObject.get(KEY_BIG_DECIMAL));
        assertEquals(expectedObject.get(KEY_BOOLEAN), actualObject.get(KEY_BOOLEAN));
    }

    @Test
    public void testDeserializationWithTypeReference() throws Exception {
        final Document document = fromXML(expectedXml);
        final Element documentElement = document.getDocumentElement();
        final Element element = getChildElement(documentElement);
        final DOMSource source = new DOMSource(element);
        final XMLStreamReader xmlStreamReader = XMLInputFactory.newFactory().createXMLStreamReader(source);
        final LinkedHashMap<String, Object> actualObject = xmlMapper.readValue(xmlStreamReader, typeReference);
        // final LinkedHashMap<String, Object> actualObject = xmlMapper.readValue(expectedXml, typeReference);

        assertEquals(expectedObject.keySet(), actualObject.keySet());
        assertEquals(expectedObject.get(KEY_STRING), actualObject.get(KEY_STRING));
        assertEquals(expectedObject.get(KEY_INTEGER), actualObject.get(KEY_INTEGER));
        assertEquals(expectedObject.get(KEY_LONG), actualObject.get(KEY_LONG));
        assertEquals(expectedObject.get(KEY_FLOAT), actualObject.get(KEY_FLOAT));
        assertEquals(expectedObject.get(KEY_DOUBLE), actualObject.get(KEY_DOUBLE));
        assertEquals(expectedObject.get(KEY_BIG_INTEGER), actualObject.get(KEY_BIG_INTEGER));
        assertEquals(expectedObject.get(KEY_BIG_DECIMAL), actualObject.get(KEY_BIG_DECIMAL));
        assertEquals(expectedObject.get(KEY_BOOLEAN), actualObject.get(KEY_BOOLEAN));
    }

    private static Document fromXML(String xml) throws ParserConfigurationException, IOException, SAXException {
        try (StringReader reader = new StringReader(xml)) {
            final InputSource source = new InputSource(reader);
            return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(source);
        }
    }

    private static String toXML(Document document) throws Exception {
        final Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount";, "4");

        try (StringWriter writer = new StringWriter()) {
            final DOMSource source = new DOMSource(document);
            final StreamResult result = new StreamResult(writer);
            transformer.transform(source, result);
            return writer.toString();
        }
    }

    private static Element getChildElement(Node node) {
        if (null == node) {
            return null;
        }

        NodeList list = node.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
            Node childNode = list.item(i);
            if ((childNode.getNodeType() == Node.ELEMENT_NODE)) {
                return (Element) childNode;
            }
        }

        return null;
    }
}

Reply via email to