Jackson XML: how to serialize empty/null collections as empty node - java

I'm using Jackson XML 2.8.9 and unfortunately I cannot find any way to serialize empty/null collections as empty nodes.
Method responsible for serializing to XML:
protected byte[] toXml(final Collection<ReportView> reports) throws IOException
{
final XmlMapper mapper = new XmlMapper();
// place for code which will solve my problem
return mapper.writerWithDefaultPrettyPrinter().withRootName("report").writeValueAsBytes(reports);
}
I tried to use:
serialization inclusion:
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
serialization provider:
final XmlSerializerProvider provider = new XmlSerializerProvider(new XmlRootNameLookup());
provider.setNullValueSerializer(new JsonSerializer<Object>()
{
#Override
public void serialize(final Object value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException
{
jgen.writeString("");
}
});
mapper.setSerializerProvider(provider);
Jackson 2.9.0 EMPTY_ELEMENT_AS_NULL feature:
mapper.configure(FromXmlParser.Feature.EMPTY_ELEMENT_AS_NULL, false);
Unfortunately nothing works. Does anybody know how to achieve it?
Test method:
#Test
public void testToXml() throws IOException
{
final Map<String, Object> payload = new LinkedHashMap<>();
payload.put("amp", "&");
payload.put("empty", Collections.emptyList());
final Date date = new Date();
final ReportView reportView = new ReportView(payload, date, "system");
// when
final byte[] xmlBytes = reportService.toXml(Arrays.asList(reportView));
// then
final StringBuilder expected = new StringBuilder();
expected.append("<report>");
expected.append(" <item>");
expected.append(" <payload>");
expected.append(" <amp>&</amp>");
expected.append(" <empty></empty>");
expected.append(" </payload>");
expected.append(" <timestamp>" + date.getTime() + "</timestamp>");
expected.append(" <changingUser>system</changingUser>");
expected.append(" </item>");
expected.append("</report>");
final String xmlText = new String(xmlBytes).replace("\n", "").replace("\r", "");
assertThat(xmlText).isEqualTo(expected.toString());
}
ReportView class:
public class ReportView {
private final Map<String, Object> payload;
private final Date timestamp;
private final String changingUser;
public ReportView(Map<String, Object> payload, Date timestamp, String changingUser) {
this.payload = payload;
this.timestamp= timestamp;
this.changingUser = changingUser;
}
public String getChangingUser() {
return changingUser;
}
public Date getTimestamp() {
return timestamp;
}
public Map<String, Object> getPayload() {
return payload;
}
}
I prepared a repository with example code: https://github.com/agabrys/bugs-reports/tree/master/jackson-xml/empty-elements-serialization
EDIT:
I extended the test toXml method and did some code cleanup.
I also tried to create a solution based on Module and SerializerModifier. Unfortunately both ended with failure. I created an issue in jackson-dataformat-xml backlog:
NPE after overriding map serializer with custom implementation (XmlBeanSerializerModifier.modifyMapSerializer)
EDIT:
I've got a hint how to solve problem with exception (see NPE after overriding map serializer with custom implementation (XmlBeanSerializerModifier.modifyMapSerializer)) but still it does not solve problem with missing empty/null values.

I needed to tackle the same issue, and here's how I got it working:
First I create a serializer that serializes nulls as empty string:
public class NullAsEmptyStringSerializer extends JsonSerializer<Object> {
static final JsonSerializer<Object> INSTANCE = new NullAsEmptyStringSerializer();
private static final String EMPTY_STRING = "";
private final StringSerializer stringSerializer = new StringSerializer();
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
stringSerializer.serialize(EMPTY_STRING, gen, serializers);
}
}
Then I create a serializer modifier, that overwrites the null serializer of the bean properties with my new serializer:
public class NullToEmptyPropertySerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (BeanPropertyWriter beanProperty : beanProperties) {
beanProperty.assignNullSerializer(NullAsEmptyStringSerializer.INSTANCE);
}
return beanProperties;
}
}
And finally, I configure the xml mapper to use my modifier:
NullToEmptyPropertySerializerModifier modifier = new NullToEmptyPropertySerializerModifier();
SerializerFactory serializerFactory = BeanSerializerFactory.instance.withSerializerModifier(modifier);
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.setSerializerFactory(serializerFactory);
Trying to see if it's working for strings and objects (Person and Dog are dummy data holder objects):
Dog dog = new Dog("bobby");
Person person = new Person("utku", null, 29, null);
String serialized = xmlMapper.writeValueAsString(person);
System.out.println(serialized);
Gives the following output:
<Person><name>utku</name><address></address><age>29</age><dog></dog></Person>

Related

Jackson how map one Pojo field to 2 (json) fields (same content, different name)

I use Jackson to serialise POJOs into CSV. Now we need to change the naming for certain fields to snake_case. This is easily done by #JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class).
For compatibility reasons we need some of the renamed fields also with their old name.
E.g.:
public class Pojo {
private int someField;
}
Default will serialise to "someField", SnakeCaseStrategy will serialise to "some_field".
How to get serialization with both?:
{
"someField" : "one",
"some_field" : "one"
}
My first try was a mixin:
public abstract class PojoFormat {
#JsonProperty("someField")
abstract String getSomeField();
}
but this effectively only undoes the naming strategy change.
So how to copy a field in serialization - preferable not by changing the Pojo (this copied fields should be removed when all clients can cope with it).
Little update:
in my real class there some nested class that use JsonUnwrapped and the doc stated that this is not working with custom serializer (didn't know that this makes a difference here).
Well, I have never seen this before, I would be very happy if someone here in this site knows how.
The easy way, in my opinion, is to use a Custom Serializer.
For example:
Using the #JsonSerialize annotation
Register a module
Dynamic Serializer with Reflection
#JsonSerialize annotation
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
#JsonSerializer(using=PojoSerializer.class)
class Pojo {
private String myValue;
// getters and setters
}
class PojoSerializer extends StdSerializer<Pojo> {
public PojoSerializer() {
super(Pojo.class);
}
#Override
public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("myValue", value.getMyValue());
gen.writeStringField("my_value", value.getMyValue());
gen.writeEndObject();
}
}
Module
static class Pojo {
private String myValue;
public String getMyValue() {
return myValue;
}
public Pojo setMyValue(String myValue) {
this.myValue = myValue;
return this;
}
}
static class PojoSerializer extends StdSerializer<Pojo> {
public PojoSerializer() {
super(Pojo.class);
}
#Override
public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("myValue", value.getMyValue());
gen.writeStringField("my_value", value.getMyValue());
gen.writeEndObject();
}
}
public static void main(String[] args) throws JsonProcessingException {
final ObjectMapper mapper = new ObjectMapper();
final SimpleModule module = new SimpleModule("PojoModule");
module.addSerializer(Pojo.class, new PojoSerializer());
mapper.registerModule(module);
final Pojo pojo = new Pojo();
pojo.setMyValue("This is the value of my pojo");
System.out.println(mapper.writeValueAsString(pojo));
}
Reflection
I write some code for you, you might want to see to get new ideias.
This works as a generic way(just to not write several serializers).
// The serializer will be register in the ObjectMapper module.
static class Pojo {
private String myValue = "With snake and camel";
private String value = "Without snake case";
private String thirdValue = "snake & camel";
}
// using the annotation
#JsonSerialize(using = PojoSerializer.class)
static class Pojo2 {
private String pojoName = "Pojo 2";
private String pojo = "pojp";
}
static class PojoSerializer extends StdSerializer<Object> {
public PojoSerializer() {
super(Object.class);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
final Field[] fields = value.getClass().getDeclaredFields();
for(final Field field : fields) {
final String name = field.getName();
final String fieldValue;
try {
// Do not use this!
fieldValue = (String)field.get(value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
byte firstUpperCase = -1;
for(byte index = 0; index < name.length(); index++) {
final char caractere = name.charAt(index);
// A ascii code is 66 decimal, and 90 is the Z in decimal
if(caractere > 'A' && caractere < 'Z') {
// found the first upper
firstUpperCase = index;
break;
}
}
// writes the normal field name
gen.writeStringField(name, fieldValue);
// if the name is in camel case, we will write in snake case too.
if(firstUpperCase != -1) {
final char lowerLetter = (char)((int) name.charAt(firstUpperCase) + 32);
final String left = name.substring(0, firstUpperCase);
final String right = String.format("%c%s",lowerLetter, name.substring(firstUpperCase + 1));
gen.writeStringField(String.format("%s_%s", left, right), fieldValue);
}
}
gen.writeEndObject();
}
}
You can try to use JsonAnyGetter annotation and define for every POJO extra mapping for backward compatibility.
Let's create a simple interface:
interface CompatibleToVer1 {
#JsonAnyGetter
Map<String, Object> getCompatibilityView();
}
and two classes which implement it:
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class RootPojo implements CompatibleToVer1 {
private int rootId;
#JsonUnwrapped
private SomePojo pojo;
#Override
public Map<String, Object> getCompatibilityView() {
return Collections.singletonMap("rootId", rootId);
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class SomePojo implements CompatibleToVer1 {
private int someField;
private String someName;
#Override
public Map<String, Object> getCompatibilityView() {
Map<String, Object> extra = new LinkedHashMap<>();
extra.put("someField", someField);
return extra;
}
}
As you can see, I defined extra columns for each POJO with custom names. Serialising to JSON is straightforward:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
SomePojo pojo = new SomePojo(123, "Tom");
mapper.writeValue(System.out, new RootPojo(1, pojo));
Above code prints:
{
"root_id" : 1,
"some_field" : 123,
"some_name" : "Tom",
"someField" : 123,
"rootId" : 1
}
But for CSV we need to create extra configuration:
CsvMapper csvMapper = CsvMapper.builder().build();
CsvSchema pojoExtraScheme = CsvSchema.builder()
.addColumn("someField")
.build();
CsvSchema rootExtraScheme = CsvSchema.builder()
.addColumn("rootId")
.build();
CsvSchema compatibleSchema = CsvSchema.emptySchema()
.withHeader()
.withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
.withColumnsFrom(rootExtraScheme)
.withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
.withColumnsFrom(pojoExtraScheme);
SomePojo tom = new SomePojo(123, "Tom");
SomePojo jerry = new SomePojo(124, "Jerry");
List<RootPojo> pojos = Arrays.asList(new RootPojo(1, tom), new RootPojo(2, jerry));
ObjectWriter writer = csvMapper.writer(compatibleSchema);
System.out.println(writer.writeValueAsString(pojos));
Above code prints:
some_field,some_name,root_id,rootId,someField
123,Tom,1,1,123
124,Jerry,2,2,124
If you do not want to specify extra columns two times you can implement builder method based on our interface:
CsvSchema createSchemaFor(CompatibleToVer1 entity) {
CsvSchema.Builder builder = CsvSchema.builder();
entity.getCompatibilityView().keySet().forEach(builder::addColumn);
return builder.build();
}
and use as below:
CsvSchema compatibleSchema = CsvSchema.emptySchema()
.withHeader()
.withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
.withColumnsFrom(createSchemaFor(new RootPojo()))
.withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
.withColumnsFrom(createSchemaFor(new SomePojo()));
Using JsonAnyGetter with CSV is really tricky and could be problematic mixing it with other annotations, take a look at: Could please add JsonAnyGetter and JsonAnySetter annotations support?

How to deserialize a JSON object to a Java collection using Jackson in a Spring application that registered the DefaultScalaModule?

I am authoring a Java library that provides REST endpoints through Spring controllers. The payload of one the endpoint is an instance of my JavaRoutine class, for which I provide a JSON serializer/deserializer pair. Here it is (slightly simplified):
#JsonSerialize(using = JavaRoutine.Serializer.class)
#JsonDeserialize(using = JavaRoutine.Deserializer.class)
public class JavaRoutine {
private final String jobId;
private final List<Object> inputValues;
private final List<ExpressionType> inputTypes; // ExpressionType is defined in my lib
public JavaRoutine(String jobId) {
this.jobId = jobId;
this.inputValues = new ArrayList<>();
this.inputTypes = new ArrayList<>();
}
public String getJobId() { return jobId; }
public void addInput(Object value) {
inputValues.add(value);
inputTypes.add(value == null ? null : ExpressionType.getTypeForValue(value));
}
public static class Serializer extends StdSerializer<JavaRoutine> {
private static final ObjectMapper mapper = new ObjectMapper();
public Serializer() {
super(JavaRoutine.class);
}
#Override
public void serialize(JavaRoutine routine, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("jobId", routine.jobId);
gen.writeArrayFieldStart("inputs");
int inputCount = routine.inputValues.size();
for (int i = 0; i < inputCount; i++) {
gen.writeStartObject();
gen.writeStringField("type", mapper.writeValueAsString(routine.inputTypes.get(i)));
gen.writeStringField("value", mapper.writeValueAsString(routine.inputValues.get(i)));
gen.writeEndObject();
}
gen.writeEndArray();
gen.writeEndObject();
}
}
public static class Deserializer extends StdDeserializer<JavaRoutine> {
private static final ObjectMapper mapper = new ObjectMapper();
public Deserializer() {
super(JavaRoutine.class);
}
#Override
public JavaRoutine deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Map<String, Object> fields = p.readValueAs(new TypeReference<Map<String, Object>>() {});
JavaRoutine routine = new JavaRoutine((String) fields.get("jobId");
List<Map<String, String>> inputs = (List<Map<String, String>>) fields.get("inputs");
for (Map<String, String> input: inputs) {
ExpressionType inputType = mapper.readValue(input.get("type"), ExpressionType.class);
Object inputValue = inputType == null ? null : mapper.readValue(input.get("value"), inputType.getJavaType());
routine.addInput(inputValue);
}
return routine;
}
}
}
This works. Except when the application that links the library has registered the Jackson module for Scala, which it needs for its own purpose. (In short, the aim of this Jackson module is to deserialize JSON structures into Scala collections and not into Java ones.) As a consequence, the call to p.readValueAs() deserializes the array of "inputs" as a Scala list, which causes the cast to List<Map<String, String>> two lines later to fail.
What solution would you recommend?
Have not tried you example. But running on Kubernetes with multiple Nodes in Google and got the the strange scala collections object when jumping between nodes.
This helped me half way.
Try creating mapper like below in (my guess) the Deserializer.
ObjectMapper mapper.registerModule(new DefaultScalaModule());
Also having some problems with the scala mappings. For me now the order is not kept. So Lists and Maps (LinkedHashMap) will loose the original order. :(

Serialize List to xml with Jackson without Annotation?

I'm looking for a way to (de-)serialize a List of items without using Annotations in Jackson. Is this possible? What I'm doing up to now is trying to replace the <item>-tag with a tag telling about the item's class, but no avail. And even if this worked, I'm not sure whether Jackson would offer a way to process this tag information.
To give a better of what I'm aiming at, here's a sample:
public class JacksonTest {
private static class ListElement {
private boolean value;
// getters, setters, constructors omitted
}
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true));
final ObjectMapper mapper = new XmlMapper();
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final String listString = mapper.writerFor(listJavaType).writeValueAsString(existing);
System.out.println(listString);
// "<List><item><value>true</value></item></List>"
}
}
So, the result is <List><item><value>true</value></item></List>, while I want the <item>-tag to be replaced with the (qualified) class name or offering a type-attribute.
Of course, even this would not help if there's no way in Jackson to process this class name.
Do I have reached a dead end here or is there a way to go?
You can define your own JsonSerializer (also used for XML) and add it to a JacksonXmlModule.
ToXmlGenerator has a setNextName function that allows you to override the default item name
private class MyListSerializer extends JsonSerializer<List> {
#Override
public void serialize(List list, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
for (Object obj : list) {
if (jsonGenerator instanceof ToXmlGenerator) {
ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
String className = obj.getClass().getSimpleName();
xmlGenerator.setNextName(new QName(className));
}
jsonGenerator.writeObject(obj);
// this is overridden at the next iteration
// and ignored at the last
jsonGenerator.writeFieldName("dummy");
}
}
#Override
public Class<List> handledType() {
return List.class;
}
}
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true));
JacksonXmlModule module = new JacksonXmlModule();
module.addSerializer(new MyListSerializer());
final ObjectMapper mapper = new XmlMapper(module);
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final ObjectWriter writer = mapper.writerFor(listJavaType);
final String listString = writer.writeValueAsString(existing);
System.out.println(listString);
// "<List><ListElement><value>true</value></ListElement></List>"
}
Okay, after some tinkering and debugging with Evertude's proposal I've figured out a solution. I'm not really happy with the serialization part and honestly I don't know why I was supposed to do it this way. When debugging I've noticed that XmlGenerator::setNextName is required to be called once but does not have any effect on the next call, so I had to implement a switch there and set the field name for the next item in the loop directly.
I'ld be glad if somebody has an idea what I'm doing wrong, but at least my attempt is working for now:
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true), new ListElement(false));
JacksonXmlModule module = new JacksonXmlModule();
module.addSerializer(new MyListSerializer());
final ObjectMapper mapper = new XmlMapper(module);
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final ObjectWriter writer = mapper.writerFor(listJavaType);
final String listString = writer.writeValueAsString(existing);
module.addDeserializer(List.class, new MyListDeserializer());
List<ListElement> deserialized = mapper.readValue(listString, List.class);
assertEquals(existing, deserialized); // provided there're proper hash() and equals() methods
}
private class MyListSerializer extends JsonSerializer<List> {
#Override
public void serialize(List list, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
boolean done = false;
for (Object obj : list) {
if (jsonGenerator instanceof ToXmlGenerator) {
ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
String className = obj.getClass().getSimpleName();
// weird switch
if (!done) xmlGenerator.setNextName(new QName(className));
else jsonGenerator.writeFieldName(className);
done = true;
}
jsonGenerator.writeObject(obj);
}
}
#Override
public Class<List> handledType() {
return List.class;
}
}
private class MyListDeserializer extends JsonDeserializer<List> {
#Override
public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
List<Object> items = new ArrayList<>();
JsonToken nextToken;
while ((nextToken = p.nextToken()) != JsonToken.END_OBJECT) {
String currentName = p.currentName();
try {
String className = "my.test.project.JacksonCustomSerializer$" + currentName;
Class<?> loadClass = getClass().getClassLoader().loadClass(className);
p.nextToken();
items.add(p.readValueAs(loadClass));
} catch (ClassNotFoundException e) {
// some handling
}
}
return items;
}
#Override
public Class<List> handledType() {
return List.class;
}
}

JSon - custom key serialization of nested maps

I have a nested Map<StructureNode, Map<String, String>> for which I need a custom key serializer & deserializer (StructureNode contains references to other objects which are needed to function as key for this map). I used the following method for this:
Jackson Modules for Map Serialization
Giving the following result. Custom Serializer:
public class StructureNodeKeySerializer extends JsonSerializer<StructureNode> {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public void serialize(StructureNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
StringWriter writer = new StringWriter();
mapper.writeValue(writer, value.copyUpwards());
gen.writeFieldName(writer.toString());
}
}
Custom deserializer:
public class StructureNodeKeyDeserializer extends KeyDeserializer {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
return mapper.readValue(key, StructureNode.class);
}
}
Usage:
#JsonDeserialize(keyUsing = StructureNodeKeyDeserializer.class) #JsonSerialize(keyUsing = StructureNodeKeySerializer.class)
private Map<StructureNode, String> structureIds;
#JsonDeserialize(keyUsing = StructureNodeKeyDeserializer.class) #JsonSerialize(keyUsing = StructureNodeKeySerializer.class)
private Map<StructureNode, Map<String, String>> metadata;
This correctly serializes a Map<StructureNode, String>, but applied to a nested Map<StructureNode, Map<String, String>>, it gives the following error:
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: java.lang.String cannot be cast to structure.StructureNode
Jackson seems to be using the same custom serialization method for the "sub-map". Is there a good way to solve this problem, without replacing the "sub-map" with another custom (non-Map) object?
You can fix this with
public static class Bean{
#JsonSerialize(using = MapStructureNodeKeySerializer.class)
public Map<StructureNode, Map<String, String>> metadata;
}
And implement your serializer a little bit differently:
public static class MapStructureNodeKeySerializer
extends JsonSerializer<Map<StructureNode, Object>> {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public void serialize(Map<StructureNode, Object> value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeStartObject();
for(Map.Entry<StructureNode, Object> val: value.entrySet()){
// your custom serialization code here
StringWriter writer = new StringWriter();
mapper.writeValue(writer, val.getKey().copyUpwards());
gen.writeObjectField(writer.toString(), val.getValue());
}
gen.writeEndObject();
}
}
Or if you want to keep keyUsing = StructureNodeKeySerializer.class
public static class Bean{
#JsonSerialize(keyUsing = StructureNodeKeySerializer.class)
public Map<StructureNode, Map<String, String>> metadata;
}
You can implement it like:
public static class StructureNodeKeySerializer extends JsonSerializer {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public void serialize(Object value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
if (value instanceof StructureNode){ // <= type of 1-st level Map key
// your custom serialization code here
StringWriter writer = new StringWriter();
mapper.writeValue(writer, ((StructureNode)value).copyUpwards());
gen.writeFieldName(writer.toString());
}else if(value instanceof String){ // <= type of 2-nd level Map key
gen.writeFieldName((String) value);
}
}
}
If you want to serialize it more generically as keySerializer, you can rewrite the else clause as follows
if (value instanceof StructureNode) {
// ...
} else {
serializers
.findKeySerializer(value.class, null)
.serialize(value, gen, serializers);
}

Jackson Modules for Map Serialization

I have a Class that contains a Map (with non String key) and some other fields.
public class MyClass() {
private Map<KeyObject, OtherObject> map;
private String someField;
public MyClass(Map<KeyObject, OtherObject> map, String someField) {
this.map = map;
this.someField = someField;
}
// Getters & Setters
}
I would like to serialize and deserialize this class using Jackson.
I saw a different ways of doing that and decided to try using jackson modules.
I followed this post and extended JsonDeserializer and JsonSerializer. The problem is that those classes should be typed, so it should look like
public class keyDeserializer extends JsonDeserializer<Map<KeyObject, OtherObject>> {
...
}
The same for the KeySerializer.
Then adding to the module:
module.addSerializer(new keySerializer());
module.addDeserializer(Map.class, new keyDeserializer());
But this is wrong apparently since I'm getting an exception:
keySerializer does not define valid handledType() -- must either register with method that takes type argument or make serializer extend 'org.codehaus.jackson.map.ser.std.SerializerBase'
I could have my serializer and deserializer to be typed to MyClass, but then I had to manually parse all of it, which is not reasonable.
UPDATE:
I managed to bypass the module creation in the code by using annotations
#JsonDeserialize(using = keyDeserializer.class)
#JsonSerialize(using = keySerializer.class)
private Map<KeyObject, OtherObject> map;
But then I have to serialize/deserialize the whole map structure on my own from the toString() output. So tried a different annotation:
#JsonDeserialize(keyUsing = MyKeyDeserializer.class)
private Map<KeyObject, OtherObject> map;
Where MyKeyDeserializer extends org.codehaus.jackson.map.KeyDeserializer and overriding the method
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException {...}
Then manually deserializing my key but again from the toString() output of my key class.
This is not optimal (this dependency on the toString() method). Is there a better way?
Ended up using this serializer:
public class MapKeySerializer extends SerializerBase<Object> {
private static final SerializerBase<Object> DEFAULT = new StdKeySerializer();
private static final ObjectMapper mapper = new ObjectMapper();
protected MapKeySerializer() {
super(Object.class);
}
#Override
public JsonNode getSchema(SerializerProvider provider, Type typeHint) throws JsonMappingException {
return DEFAULT.getSchema(provider, typeHint);
}
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
if (null == value) {
throw new JsonGenerationException("Could not serialize object to json, input object to serialize is null");
}
StringWriter writer = new StringWriter();
mapper.writeValue(writer, value);
jgen.writeFieldName(writer.toString());
}
}
And this Deserializer:
public class MapKeyDeserializer extends KeyDeserializer {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return mapper.readValue(key, MyObject.class);
}
}
Annotated my Map:
#JsonDeserialize(keyUsing = MapKeyDeserializer.class)
#JsonSerialize(keyUsing = MapKeySerializer.class)
private Map<KeyObject, OtherObject> map;
This is the solution that worked for me, hope this helps other.

Categories