Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson Serialization: Different formats for XML and JSON

I use Jackson to serialize/deserialize my application's model into both JSON and XML (need them both).

Model classes:

@JacksonXmlRootElement
public class Data { 
    @JsonProperty("attributes")
    @JsonDeserialize(using = AttributesDeserializer.class)
    @JsonSerialize(using = AttributesSerializer.class)
    @JacksonXmlElementWrapper
    private Map<Key, Map<String, Attribute>> attributes;

....

public class Key {
        private Integer id;
        private String name;

....

public class Attribute {
    private Integer id;
    private Integer value;
    private String name;

I need my JSON to look like this:

 {
  "attributes": [
    {
      "key": {
        "id": 10,
        "name": "key1"
      },
      "value": {
        "numeric": {
          "id": 1,
          "value": 100,
          "name": "numericAttribute"
        },
        "text": {
          "id": 2,
          "value": 200,
          "name": "textAttribute"
        }
      }
    },
    {
      "key": {
        "id": 20,
        "name": "key2"
      },
      "value": {
        "numeric": {
          "id": 1,
          "value": 100,
          "name": "numericAttribute"
        },
        "text": {
          "id": 2,
          "value": 200,
          "name": "textAttribute"
        }
      }
    }
  ]
}

And my XML something like this:

<Data>
    <attributes>
        <key>
            <id>10</id>
            <name>key1</name>
        </key>
        <value>
            <numeric>
                <id>1</id>
                <value>100</value>
                <name>numericAttribute</name>
            </numeric>
            <text>
                <id>2</id>
                <value>200</value>
                <name>textAttribute</name>
            </text>
        </value>
        <key>
            <id>20</id>
            <name>key2</name>
        </key>
        <value>
            <numeric>
                <id>1</id>
                <value>100</value>
                <name>numericAttribute</name>
            </numeric>
            <text>
                <id>2</id>
                <value>200</value>
                <name>textAttribute</name>
            </text>
        </value>
    </attributes>
</Data>

I am obtaining both the required JSON and XML with the custom serializer:

public class AttributesSerializer extends JsonSerializer<Map<Key, Map<String, Attribute>>> {

    @Override
    public void serialize(Map<Key, Map<String, Attribute>> map, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
        jsonGenerator.writeStartArray();
        for (Map.Entry<Key, Map<String, Attribute>> entry : map.entrySet()) {
            jsonGenerator.writeStartObject();
            jsonGenerator.writeObjectField("key", entry.getKey());
            jsonGenerator.writeObjectFieldStart("value");
            for (Map.Entry<String, Attribute> attributesEntry : entry.getValue().entrySet()) {
                jsonGenerator.writeObjectField(attributesEntry.getKey(), attributesEntry.getValue());
            }
            jsonGenerator.writeEndObject();
            jsonGenerator.writeEndObject();
        }
        jsonGenerator.writeEndArray();
    }
}

And the deserialization works fine for the JSON with the custom deserializer:

public class AttributesDeserializer extends JsonDeserializer<Map<Key, Map<String, Attribute>>> {
    @Override
    public Map<Key, Map<String, Attribute>> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        if (node.size() == 0) {
            return null;
        }
        ObjectMapper om = new ObjectMapper();
        Map<Key, Map<String, Attribute>> attributes = new HashMap<>();
        node.forEach(jsonNode -> {
            Map<String, Attribute> attributesMap = new HashMap<>();
            JsonNode keyNode = jsonNode.get("key");
            Key key = om.convertValue(keyNode, Key.class);
            JsonNode valueNode = jsonNode.get("value");
            Iterator<Map.Entry<String, JsonNode>> attributesIterator = valueNode.fields();
            while(attributesIterator.hasNext()) {
                Map.Entry<String, JsonNode> field = attributesIterator.next();
                Attribute attribute = om.convertValue(field.getValue(), Attribute.class);
                attributesMap.put(field.getKey(), attribute);
            }
            attributes.put(key, attributesMap);
        });


        return attributes;
    }
}

While everything is fine for the JSON, for the XML the application crashes in the deserialization:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: ro.alexsvecencu.jackson.Data["attributes"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:388)
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:348)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1599)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:278)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:140)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2740)
    at ro.alexsvecencu.jackson.Main.main(Main.java:27)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.NullPointerException
    at ro.alexsvecencu.jackson.AttributesDeserializer.lambda$deserialize$0(AttributesDeserializer.java:29)
    at ro.alexsvecencu.jackson.AttributesDeserializer$$Lambda$1/1709366259.accept(Unknown Source)
    at java.lang.Iterable.forEach(Iterable.java:75)
    at ro.alexsvecencu.jackson.AttributesDeserializer.deserialize(AttributesDeserializer.java:24)
    at ro.alexsvecencu.jackson.AttributesDeserializer.deserialize(AttributesDeserializer.java:15)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:499)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:101)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:276)
    ... 9 more

What's happening is that my custom deserializer crashes for the XML, because obviously it's not interpreting all the attributes as an 'array' and when I'm going through the jsonNode's children it will iterate through the keys/values. Also, through debugging I notice that the deserializer is just called for the LAST tag of attributes from XML.

Is there any way to tell Jackson to use specific custom deserializers/serializers that are different for XML and JSON? That's one way in which I think this could be solved.

My XML could be formatted a bit different (I'm not really constrained in it's form, but the JSON has to keep that format). With this flexibility, do you see any alternative to solving my issue? I could just use something different for XML, like JAXB, but I'm pretty much constrained to use Jackson for both.

like image 271
alexsvecencu Avatar asked Jan 15 '17 18:01

alexsvecencu


1 Answers

I have a partial solution for you. With Jackson mixin feature, it is possible to have different custom deserializers/serializers for XML and JSON

First, you create another POJO class that has properties with the same name as the ones of Data class, with the different annotations for custom deserializers/serializers

@JacksonXmlRootElement
public static class XmlData
{
    @JsonProperty("attributes")
    @JsonDeserialize(using = XmlAttributesDeserializer.class)  // specify different serializer
    @JsonSerialize(using = XmlAttributesSerializer.class)  // specify different deserializer
    @JacksonXmlElementWrapper
    public Map<Key, Map<String, Attribute>> attributes;
}

Next, you create a Jackson Module that associates the Data class with the mixin XmlData class,

@SuppressWarnings("serial")
public static class XmlModule extends SimpleModule
{
    public XmlModule()
    {
        super("XmlModule");
    }

    @Override
    public void setupModule(SetupContext context)
    {
        context.setMixInAnnotations(Data.class, XmlData.class);
    }
}

Here is a test method that shows how to register the module to the mapper and dynamically serialize to different format:

public static void main(String[] args)
{
    Attribute a1 = new Attribute();
    a1.id = 1;
    a1.value = 100;
    a1.name = "numericAttribute";
    Attribute a2 = new Attribute();
    a2.id = 2;
    a2.value = 200;
    a2.name = "textAttribute";
    Map<String, Attribute> atts = new HashMap<>();
    atts.put("numeric", a1);
    atts.put("text", a2);
    Key k1 = new Key();
    k1.id = 10;
    k1.name = "key1";
    Key k2 = new Key();
    k2.id = 20;
    k2.name = "key2";
    Data data = new Data();
    data.attributes = new HashMap<>();
    data.attributes.put(k1, atts);
    data.attributes.put(k2, atts);

    ObjectMapper mapper;
    if ("xml".equals(args[0])) {
        mapper = new XmlMapper();
        mapper.registerModule(new XmlModule());
    } else {
        mapper = new ObjectMapper();
    }
    try {
        mapper.writeValue(System.out, data);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
like image 174
Sharon Ben Asher Avatar answered Oct 23 '22 05:10

Sharon Ben Asher