Reputation: 303
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.
Upvotes: 6
Views: 5862
Reputation: 1
Could it be the solution to use the only Serializer/Derializer for both Json and Xml, and inside of them check the type of JsonGenerator (json or xml) and apply the logic related to the specific format? Like that
public class AttributeSerializer extends JsonSerializer<Map<Key, Map<String, Attribute>>> {
@Override
public void serialize(Map<Key, Map<String, Attribute>> value, JsonGenerator gen, SerializerProvider serializers)
throws IOException, JsonProcessingException {
if (gen instanceof ToXmlGenerator) {
//apply logic to format xml structure
} else {
//apply logic to format json or else
}
}
}
I understand that using instaceOf
is not a good architecture, but this way we avoid duplication of dto for xml.
Upvotes: 0
Reputation: 303
Besides the solution offered by sharonbn, I've found that if you can encapsulate your field into a different class you can then register a different serializer via a module for either Json (ObjectMapper) or Xml (XmlMapper).
Here's an example in action:
public class CustomSerializers {
public static class PojoField {
PojoField() {
value = "PojoField";
}
public String value;
}
@JacksonXmlRootElement
public static class Pojo {
Pojo() {
field = new PojoField();
}
@JsonProperty("field")
public PojoField field;
}
public static class PojoFieldJSonSerializer extends JsonSerializer<PojoField> {
@Override
public void serialize(PojoField pojoField, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeObject(pojoField.value + " in JSON");
}
}
public static class PojoFieldXmlSerializer extends JsonSerializer<PojoField> {
@Override
public void serialize(PojoField pojoField, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeObject(pojoField.value + " in XMl");
}
}
public static void main(String []args) throws IOException {
Pojo pojo = new Pojo();
SimpleModule objectMapperModule = new SimpleModule();
objectMapperModule.addSerializer(PojoField.class, new PojoFieldJSonSerializer());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(objectMapperModule);
objectMapper.writeValue(new File("pojo.json"), pojo);
SimpleModule xmlMapperModule = new SimpleModule();
xmlMapperModule.addSerializer(PojoField.class, new PojoFieldXmlSerializer());
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.registerModule(xmlMapperModule);
xmlMapper.writeValue(new File("pojo.xml"), pojo);
}
}
The output for JSON will be:
{"field": "PojoField in JSON"}
The output for XML:
<Pojo>
<field>PojoField in XMl</field>
</Pojo>
Upvotes: 0
Reputation: 14328
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();
}
}
Upvotes: 4