Search code examples
javafasterxmljsonserializer

FasterXml - JsonSerializer HashMap


I'm using jackson-databind version 2.12.3 to serialize the return of an object that should return like this:

{
  "field1":"value1",
  "field2":"value2",
  "links":{
    "field":{
      "href":"/link"
    },
    "test":{
      "href":"/test"
    }
  }
}

My classes are these:

public class HrefType  {
  private String href = null;
  ...
}
public class Link extends HashMap<String, HrefType>  {
  private HrefType field = null;
  ...
}
public class MyObject  {
  private String field1 = null;
  private String field2 = null;
  private Link links = null;
  ...
}

The return is myObject:

  MyObject myObject = new MyObject();
  myObject.setField1("value1");
  myObject.setField2("value2");

  Link link = new Link();
  link.setField(new HrefType().href("/link"));
  link.put("test",new HrefType().href("/test"));

  myObject.setLinks(link);

However with the default ObjectMapper the "link.setField" is ignored and the returned json is:

{
  "field1":"value1",
  "field2":"value2",
  "links":{
    "test":{
      "href":"/test"
    }
  }
}

I tried doing some tests with JsonSerializer but couldn't do something generic for all classes that extend HashMap (these classes are generated from BerlinGroup's PSD2 YAML, so I wouldn't want to change the generated class).

Is there a generic way to do it, or should I make a serialize class for each class that extends the HashMap?


Solution

  • based on this answer I developed this generic method of making for all objects that extend a Map:

    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.util.Map;
    
    import org.springframework.util.ReflectionUtils;
    
    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.databind.JsonSerializer;
    import com.fasterxml.jackson.databind.SerializerProvider;
    
    public class MyClassSerializer extends JsonSerializer<Object> {
        private final JsonSerializer<Object> defaultSerializer;
    
        public MyClassSerializer(JsonSerializer<Object> defaultSerializer) {
            this.defaultSerializer = (defaultSerializer);
        }
    
        @SuppressWarnings({ "unchecked", "rawtypes" })
        @Override
        public void serialize(Object src, JsonGenerator gen, SerializerProvider provider) throws IOException {
            Field[] fields = src.getClass().getDeclaredFields();
            for (Field field : fields) {
                try {
                    boolean fieldAccessible = field.isAccessible();
                    field.setAccessible(true);
    
                    Object object = ReflectionUtils.getField(field, src);
                    if (object != null && object instanceof Map) {
    
                        Field[] fieldsMap = object.getClass().getDeclaredFields();
                        Map map = (Map) object;
                        for (Field fieldMap : fieldsMap) {
                            boolean fieldMapAccessible = fieldMap.isAccessible();
                            fieldMap.setAccessible(true);
    
                            Object fieldObject = ReflectionUtils.getField(fieldMap, object);
                            if (fieldObject != null) {
                                map.put(fieldMap.getName(), fieldObject);
                            }
                            fieldMap.setAccessible(fieldMapAccessible);
                        }
                    }
                    field.setAccessible(fieldAccessible);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            defaultSerializer.serialize(src, gen, provider);
        }
    
        @Override
        public Class<Object> handledType() {
            return Object.class;
        }
    }
    

    which goes through all fields, when I find one that extends from a Map I go through all the fields of this one and add it to the Map ignoring the object's fields, so the Serializer works perfectly.

    EDIT: to Deserializer properly I do this:

    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.util.Map;
    
    import org.springframework.util.ReflectionUtils;
    
    import com.fasterxml.jackson.core.JsonParser;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.DeserializationContext;
    import com.fasterxml.jackson.databind.JsonDeserializer;
    import com.fasterxml.jackson.databind.JsonMappingException;
    import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
    
    @SuppressWarnings("rawtypes")
    public class MyClassDeserializer extends JsonDeserializer implements ResolvableDeserializer {
    
        private JsonDeserializer defaultDeserializer;
    
        protected MyClassDeserializer(JsonDeserializer deserializer) {
            this.defaultDeserializer = deserializer;
        }
    
        @SuppressWarnings("unchecked")
        @Override
        public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            Object obj = defaultDeserializer.deserialize(p, ctxt);
            Field[] fields = obj.getClass().getDeclaredFields();
            for (Field field : fields) {
                try {
                    boolean fieldAccessible = field.isAccessible();
                    field.setAccessible(true);
                    Object object = ReflectionUtils.getField(field, obj);
                    if (object != null && object instanceof Map) {
    
                        Field[] fieldsMap = object.getClass().getDeclaredFields();
                        Map map = (Map) object;
                        for (Object key : map.keySet()) {
                            for (Field fieldMap : fieldsMap) {
                                if (fieldMap.getName().equals((String) key)) {
    
                                    if (fieldMap.getName().equalsIgnoreCase("serialVersionUID")) {
                                        continue;
                                    }
                                    boolean fieldMapAccessible = fieldMap.isAccessible();
                                    fieldMap.setAccessible(true);
    
                                    Object fieldObject = ReflectionUtils.getField(fieldMap, object);
                                    if (fieldObject == null) {
                                        fieldMap.set(object, map.get(key));
                                        map.replace(key, null);
                                    }
                                    fieldMap.setAccessible(fieldMapAccessible);
                                }
                            }
                        }
                        Object[] keys = map.keySet().toArray();
                        for (int i = 0; i < keys.length; i++) {
                            if(map.get(keys[i])==null) {
                                map.remove(keys[i]);
                            }
                        }
                    }
                    field.setAccessible(fieldAccessible);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            return obj;
        }
    
        @Override
        public void resolve(DeserializationContext ctxt) throws JsonMappingException {
            ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
        }
    
    }