Why ObjectMapper changes Date type into Long

Viewed 1319

I am trying to get a map from the object using Jackson ObjectMapper:

    ObjectMapper oMapper = ObjectMapperWithDate.getObjectMapper();
    Map<String, Object> map = oMapper.convertValue(obj, Map.class);

I have problems with Date fields, for in the map they are becoming Long objects.

I have added de/serializers, as in ObjectMapper changes Date to String

public class ObjectMapperWithDate {
    @Bean
    public static ObjectMapper getObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        mapper.registerModule(
                new SimpleModule("foo")
                        .addDeserializer(Date.class, new DateDeserializer())
                        .addSerializer(Date.class, new DateSerializer())
        );
        return mapper;
    }
    public static class DateSerializer extends StdScalarSerializer<Date> {
        public DateSerializer() {
            super(Date.class);
        }
        @Override
        public void serialize(Date value, JsonGenerator gen, SerializerProvider provider)
                throws IOException {
            DateFormat formatter = new SimpleDateFormat("dd-MM-yyyy", Locale.ENGLISH);
            String output = formatter.format(value);
            gen.writeString(output);
        }
    }
    public static class DateDeserializer extends StdScalarDeserializer<Date> {
        public DateDeserializer() {
            super(Date.class);
        }
        @Override
        public Date deserialize(JsonParser p, DeserializationContext ctxt)
                throws IOException {
            try {
                DateFormat formatter = new SimpleDateFormat("dd-MM-yyyy", Locale.ENGLISH);
                return formatter.parse(p.getValueAsString());
            } catch (Exception e) {
                return null;
            }
        }
    }
}

Of course, the call for the mapper looks a bit different:

    ObjectMapper oMapper = ObjectMapperWithDate.getObjectMapper();
    Map<String, Object> map = oMapper.convertValue(obj, Map.class);

Now the Date objects become String object in the map. With the dates properly represented in them. But I need them to remain to be of the Date type. What is interesting, if I put breakpoints in the deserialiser, it is never reached. So, the deserializer is never reached, I think, it is because the mapper after serializing makes Date a String or a Long, depending on SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, and never recognizes the Date's in the time of deserialization.

How can I let Date properties remain Date ones after mapping? I need them to be recognized.

BTW, the BigDecimal properties are turned into Double ones. It seems to be the similar problem, but these two types are not of much difference to my further work.

2 Answers

Because you defined map's type of values as Object, Jackson doesn't select your custom deserializer of type Date and uses its default deserializer where it converts all types to basic ones (like long, String, LinkedHashMap etc.).

If you only had Date fields in your object, you could just change the 2nd argument of convertValue method:

Map<String, Date> map = oMapper.convertValue(obj, new TypeReference<Map<String, Date>>() {});

But obviously it's not your case, so the most straightforward way to do it for an object with different types of fields is to change your deserializer type to Object and parse all data inside it manually:

    public static class DateDeserializer extends StdScalarDeserializer<Object> {
        public DateDeserializer() {
            super(Object.class);
        }
        @Override
        public Object deserialize(JsonParser p, DeserializationContext ctxt)
                throws IOException {
            String valueAsString = p.getValueAsString();
            try {
                DateFormat formatter = new SimpleDateFormat("dd-MM-yyyy", Locale.ENGLISH);
                return formatter.parse(valueAsString);
            } catch (Exception e) {
                //you could add extra logic to parse other types
                return valueAsString;
            }
        }
    }

Also, don't forget to replace the 1st argument of .addDeserializer with Object.class

For more exotic ways to do it, please check this article: http://robertmarkbramprogrammer.blogspot.com/2018/05/de-serialise-json-string-to-map-with.html

You should use a POJO to represent your data, instead of a Map. Since a Map is ambiguous key-value pairs, there's no type information for each field and Jackson will read values without making any inferences about their type information. By modeling your data in a defined structure, Jackson will read values into the defined types.

public class DataTransferObject
{
   Date date;
   BigDecimal decimal;

   public Date getDate ()
   {
      return date;
   }

   public void setDate ( Date date )
   {
      this.date = date;
   }

   public BigDecimal getDecimal ()
   {
      return decimal;
   }

   public void setDecimal ( BigDecimal decimal )
   {
      this.decimal = decimal;
   }
}
objectMapper.convertValue(obj, DataTransferObject.class);

With this approach, you may use Jackson's serialization features to convert dates using strings or integers. A custom (de)serializer isn't necessary.

Related