I have seen answers like this one that show the use of TypeFactory.constructMapType(...) to de-serialise a JSON string to a map where the key/value combinations are other than String. I have a situation where I have strings that should de-serialise to multiple different types, not just one.
I realise that one solution would be define my own class and not use Map, but I am wondering if I can use pure configuration instead?
Here is my test code.
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.joda.JodaModule;
public class JodaTimeMapTest {
   public static void main(final String[] args) throws Exception {
      // Map with dates.
      final DateTime now = new DateTime().withZone(DateTimeZone.UTC);
      final LocalDateTime nowLocal = new LocalDateTime();
      final LocalDateTime notNowLocal = new LocalDateTime(2007, 3, 25, 2, 30, 0);
      final Map<String, Object> dateMap = new HashMap<>();
      dateMap.put("now", now);
      dateMap.put("nowLocal", nowLocal);
      dateMap.put("notNowLocal", notNowLocal);
      // Serialise map to string.
      final ObjectMapper mapper = mapper();
      final String dateMapJson = mapper.writeValueAsString(dateMap);
      // De-serialise string to map.
      final TypeFactory typeFactory = mapper.getTypeFactory();
      final MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, Object.class);
      final HashMap<String, Object> dateMapFromJson = mapper.readValue(dateMapJson, mapType);
      // First one has dates, second has strings.
      printMap(dateMap);
      printMap(dateMapFromJson);
   }
   private static void printMap(final Map<String, Object> map) {
      System.out.println(map.entrySet().stream().map(entry -> {
         return entry.getKey() + ", type = " + entry.getValue().getClass().getName() + ", value = " + entry.getValue();
      }).collect(Collectors.joining(System.lineSeparator())));
   }
   private static ObjectMapper mapper() {
      final ObjectMapper mapper = new ObjectMapper();
      mapper.registerModule(new JodaModule());
      mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
      return mapper;
   }
}
The output of this class shows that reading in, Jakcson can only assume these are strings:
now, type = org.joda.time.DateTime, value = 2018-05-04T09:10:26.063Z
notNowLocal, type = org.joda.time.LocalDateTime, value = 2007-03-25T02:30:00.000
nowLocal, type = org.joda.time.LocalDateTime, value = 2018-05-04T19:10:26.193
now, type = java.lang.String, value = 2018-05-04T09:10:26.063Z
notNowLocal, type = java.lang.String, value = 2007-03-25T02:30:00.000
nowLocal, type = java.lang.String, value = 2018-05-04T19:10:26.193
Sample Solution
Based on the answer aussie gave, here is a solution that works for me. In my example, the map key is all I need to determine what sort of Joda date/time class the value is.
First is my implementation of the de-serialiser aussie told me about.
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.joda.time.DateTime;
import org.joda.time.LocalDateTime;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
/** De-serialise values from a map that contains Joda times and strings. */
public class JodaMapDeserialiser extends StdDeserializer<Object> {
   /** Mapping between keys in the map to a type of Joda time. */
   enum DateType {
      DATE_TIME("now"), LOCAL_DATE_TIME("notNowLocal", "nowLocal");
      final List<String> keys;
      DateType(final String... keys) {
         this.keys = Arrays.asList(keys);
      }
      public static DateType forKeyString(final String keyString) {
         return Stream.of(values()).filter(dateTypes -> dateTypes.keys.contains(keyString)) //
               .findFirst().orElse(null);
      }
   }
   public JodaMapDeserialiser() {
      super(Object.class);
   }
   @Override
   public Object deserialize(final JsonParser p, final DeserializationContext ctxt)
         throws IOException, JsonProcessingException {
      // Each entry in the map has a key and value.
      final String value = p.readValueAs(String.class);
      final String key = p.getCurrentName();
      // Convert the value depending on what the key is.
      switch (DateType.forKeyString(key)) {
         case DATE_TIME:
            return DateTime.parse(value);
         case LOCAL_DATE_TIME:
            return LocalDateTime.parse(value);
         default:
            return value;
      }
   }
}
And here is some slightly revised testing code.
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.joda.JodaModule;
public class JodaTimeMapTest {
   public static void main(final String[] args) throws Exception {
      // Map with dates.
      final DateTime now = new DateTime().withZone(DateTimeZone.UTC);
      final LocalDateTime nowLocal = new LocalDateTime();
      final LocalDateTime notNowLocal = new LocalDateTime(2007, 3, 25, 2, 30, 0);
      final Map<String, Object> dateMap = new HashMap<>();
      dateMap.put("now", now);
      dateMap.put("nowLocal", nowLocal);
      dateMap.put("notNowLocal", notNowLocal);
      // Serialise map to string.
      final ObjectMapper mapper = mapper();
      final String dateMapJson = mapper.writeValueAsString(dateMap);
      // De-serialise string to map.
      final TypeFactory typeFactory = mapper.getTypeFactory();
      final MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, Object.class);
      final HashMap<String, Object> dateMapFromJson = mapper.readValue(dateMapJson, mapType);
      // First one has dates, second has strings.
      System.out.println("Actual map.");
      printMap(dateMap);
      System.out.println("Map de-serialised from JSON.");
      printMap(dateMapFromJson);
      System.out.println("Maps are equal: " + dateMap.equals(dateMapFromJson));
   }
   private static void printMap(final Map<String, Object> map) {
      System.out.println(map.entrySet().stream().map(entry -> {
         return "  " + entry.getKey() + ", type = " + entry.getValue().getClass().getName() + ", value = "
               + entry.getValue();
      }).collect(Collectors.joining(System.lineSeparator())));
   }
   private static ObjectMapper mapper() {
      final ObjectMapper mapper = new ObjectMapper();
      mapper.registerModule(new JodaModule());
      mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
      final SimpleModule dateDeserializerModule = new SimpleModule();
      dateDeserializerModule.addDeserializer(Object.class, new JodaMapDeserialiser());
      mapper.registerModule(dateDeserializerModule);
      return mapper;
   }
}
And the output is:
Actual map.
  now, type = org.joda.time.DateTime, value = 2018-05-05T04:03:20.684Z
  notNowLocal, type = org.joda.time.LocalDateTime, value = 2007-03-25T02:30:00.000
  nowLocal, type = org.joda.time.LocalDateTime, value = 2018-05-05T14:03:20.809
Map de-serialised from JSON.
  now, type = org.joda.time.DateTime, value = 2018-05-05T04:03:20.684Z
  notNowLocal, type = org.joda.time.LocalDateTime, value = 2007-03-25T02:30:00.000
  nowLocal, type = org.joda.time.LocalDateTime, value = 2018-05-05T14:03:20.809
Maps are equal: true
Finally, my maven dependencies (joda time is included in jackson-datatype-joda).
<dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-core</artifactId>
   <version>2.9.5</version>
</dependency>
<dependency>
   <groupId>com.fasterxml.jackson.datatype</groupId>
   <artifactId>jackson-datatype-joda</artifactId>
   <version>2.9.5</version>
</dependency>
Other options
Overall, the options I found:
- Create type definition for a single type combination: HashmapwithStringkeys andDateTimevalues.
- Create a custom class to map key/values to.
- Create a de-serialiser to define rules for how to translate string to object.
To further explore the different options I found, I wrote up this blog post.
 
    