Saturday, May 05, 2018

De-serialise JSON string to map with multiple value types

  1. Map of String to LocalDateTime
  2. Map of String to multiple date types using a custom class
  3. Map of String to multiple date types using a custom de-serialiser
  4. Maven dependencies

I have a map that contains dates and strings. I convert it to a JSON string and later want to convert it back. In Java, the Jackson API is perfect for this. In this post I explore three methods of doing this that cater for different levels of complexity involved in how many types of value are contained in the map.

I created this blog entry because I wanted to fully explore the options I found when trying to solve this problem in an app I am working on. The solution I ended up going ahead with came about from some great feedback in this Stack Overflow post, thanks to @aussie.

To top of post.

Map of String to LocalDateTime

First, here is the map of String keys to LocalDateTime object values.

// Create dates.
final LocalDateTime nowLocal = new LocalDateTime();
final LocalDateTime notNowLocal =
      new LocalDateTime(2007, 3, 25, 2, 30, 0);

// Put them into a map.
final Map<String, LocalDateTime> dateMap = new HashMap<>();
dateMap.put("nowLocal", nowLocal);
dateMap.put("notNowLocal", notNowLocal);

Here is how I serialise it to a JSON string using Jackson.

// Create a mapper.
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JodaModule());
mapper.configure(com.fasterxml.jackson.databind
      .SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

// Serialise the map as a JSON string.
final String dateMapJson = mapper.writeValueAsString(dateMap);
System.out.println(dateMapJson);

The result of this code is below.

{"notNowLocal":"2007-03-25T02:30:00.000","nowLocal":"2018-05-05T15:57:25.108"}

This is how I de-serialise the string back into a map.

/*
 * Create a type definition of my map: something that will tell Jackson
 * I want back a HashMap whose keys are String type objects and whose
 * values are LocalDateTime objects.
 */
final TypeFactory typeFactory = mapper.getTypeFactory();
final MapType mapType = typeFactory.constructMapType(
      HashMap.class, String.class, LocalDateTime.class);

// Use the mapper from above to convert the JSON string back to a map.
final Map<String, LocalDateTime> dateMapFromJson =
      mapper.readValue(dateMapJson, mapType);

Here is a complete example of this code.

import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS;
import static java.lang.String.format;

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

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;

/**
 * Test converting a map of {@link LocalDateTime}s to a JSON string and back.
 */
public class JodaTimeMapTestOneDateType {

   public static void main(final String[] args) throws Exception {

      // Map with dates.
      final LocalDateTime nowLocal = new LocalDateTime();
      final LocalDateTime notNowLocal =
            new LocalDateTime(2007, 3, 25, 2, 30, 0);
      final Map<String, LocalDateTime> dateMap = new HashMap<>();
      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, LocalDateTime.class);
      final Map<String, LocalDateTime> dateMapFromJson =
            mapper.readValue(dateMapJson, mapType);

      // Print off the maps, JSON string and proof that the maps are equal.
      System.out.printf("Starting map.%s%n%n", mapToString(dateMap));
      System.out.printf("Map serialised to a JSON string.%n   %s%n%n",
            dateMapJson);
      System.out.printf("Map de-serialised from JSON.%s%n%n",
            mapToString(dateMapFromJson));
      System.out.printf("Maps are equal: %s%n",
            dateMap.equals(dateMapFromJson));
   }

   /**
    * @param map
    *           with strings and dates.
    * @return string that tells me what the keys, value classes and values are.
    */
   private static String mapToString(final Map<String, LocalDateTime> map) {
      return format("%n   %-13s %-30s %s", "key", "value class", "value") //
            + format("%n   %-13s %-30s %s", "---", "-----------", "-----") //
            + map.entrySet().stream().map(entry -> {
               final Object value = entry.getValue();
               return format("%n   %-13s %-30s %s", entry.getKey(),
                     value.getClass().getName(), value);
            }).collect(Collectors.joining());
   }

   /**
    * @return object that transform my maps to strings and back.
    */
   private static ObjectMapper mapper() {
      final ObjectMapper mapper = new ObjectMapper();
      mapper.registerModule(new JodaModule());
      mapper.configure(WRITE_DATES_AS_TIMESTAMPS, false);
      return mapper;

   }

}

The result of running this code is below.

Starting map.
   key           value class                    value
   ---           -----------                    -----
   notNowLocal   org.joda.time.LocalDateTime    2007-03-25T02:30:00.000
   nowLocal      org.joda.time.LocalDateTime    2018-05-05T16:16:23.572

Map serialised to a JSON string.
   {"notNowLocal":"2007-03-25T02:30:00.000","nowLocal":"2018-05-05T16:16:23.572"}

Map de-serialised from JSON.
   key           value class                    value
   ---           -----------                    -----
   notNowLocal   org.joda.time.LocalDateTime    2007-03-25T02:30:00.000
   nowLocal      org.joda.time.LocalDateTime    2018-05-05T16:16:23.572

Maps are equal: true

To top of post.

Map of String to multiple date types using a custom class

What about if my map has multiple types, not just LocalDateTime?

// Different date objects.
final DateTime now = new DateTime().withZone(DateTimeZone.UTC);
final LocalDateTime nowLocal = new LocalDateTime();
final LocalDateTime notNowLocal =
      new LocalDateTime(2007, 3, 25, 2, 30, 0);

// Make a map with the dates.
final Map<String, Object> dateMap = new HashMap<>();
dateMap.put("now", now);
dateMap.put("nowLocal", nowLocal);
dateMap.put("notNowLocal", notNowLocal);

// Create object mapper that knows how to write Jodatime dates.
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JodaModule());
mapper.configure(WRITE_DATES_AS_TIMESTAMPS, false);

// Serialise map to JSON string.
final String dateMapJson = mapper.writeValueAsString(dateMap);
System.out.println(dateMapJson);

The output of this code is below.

{"now":"2018-05-05T06:35:21.330Z","notNowLocal":"2007-03-25T02:30:00.000","nowLocal":"2018-05-05T16:35:21.441"}

The problem is how to tell Jackson about the combination of types to expect. In the previous section, I created a type definition to tell Jackson I want back a HashMap whose keys are String type objects and whose values are LocalDateTime objects.

final TypeFactory typeFactory = mapper.getTypeFactory();
final MapType mapType = typeFactory.constructMapType(
      HashMap.class, String.class, LocalDateTime.class);

There is no other constructMapType method that lets me define multiple type definitions. However, look at the JSON string in another way.

{
           "now" : "2018-05-05T06:35:21.330Z",
   "notNowLocal" : "2007-03-25T02:30:00.000",
      "nowLocal" : "2018-05-05T16:35:21.441"
}

Doesn't this JSON map look like it could fit a Java class with three date variables called now, notNowLocal and nowLocal?

public class DateTimeHolder {

   private DateTime now;
   private LocalDateTime nowLocal;
   private LocalDateTime notNowLocal;

   public Map<String, Object> buildMap() {
      final Map<String, Object> dateMap = new HashMap<>();
      dateMap.put("now", now);
      dateMap.put("nowLocal", nowLocal);
      dateMap.put("notNowLocal", notNowLocal);
      return dateMap;
   }

   // Getters and setters...

}

Now I have a class with variables whose names match the keys in my map, and whose variable types match the types of my map values. I can use this to de-serialise the JSON string, as below.

// Tell Jackson to read the string and convert it to a DateTimeHolder.
final DateTimeHolder holder =
      mapper.readValue(dateMapJson, DateTimeHolder.class);

// Which I can now convert back into a map.
final Map<String, Object> dateMapFromJson = holder.buildMap();

This works fine. Here is the complete example.

import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS;
import static java.lang.String.format;

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.datatype.joda.JodaModule;

/**
 * Test converting a map of multiple date types to a JSON string and back.
 */
public class JodaTimeMapTestUsingCustomType {

   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 DateTimeHolder holder =
            mapper.readValue(dateMapJson, DateTimeHolder.class);
      final Map<String, Object> dateMapFromJson = holder.buildMap();

      // Print off the maps, JSON string and proof that the maps are equal.
      System.out.printf("Starting map.%s%n%n", mapToString(dateMap));
      System.out.printf("Map serialised to a JSON string.%n   %s%n%n",
            dateMapJson);
      System.out.printf("Map de-serialised from JSON.%s%n%n",
            mapToString(dateMapFromJson));
      System.out.printf("Maps are equal: %s%n",
            dateMap.equals(dateMapFromJson));
   }

   /**
    * @param map
    *           with strings and dates.
    * @return string that tells me what the keys, value classes and values are.
    */
   private static String mapToString(final Map<String, Object> map) {
      return format("%n   %-13s %-30s %s", "key", "value class", "value") //
            + format("%n   %-13s %-30s %s", "---", "-----------", "-----") //
            + map.entrySet().stream().map(entry -> {
               final Object value = entry.getValue();
               return format("%n   %-13s %-30s %s", entry.getKey(),
                     value.getClass().getName(), value);
            }).collect(Collectors.joining());
   }

   /**
    * @return object that transform my maps to strings and back.
    */
   private static ObjectMapper mapper() {
      final ObjectMapper mapper = new ObjectMapper();
      mapper.registerModule(new JodaModule());
      mapper.configure(WRITE_DATES_AS_TIMESTAMPS, false);

      return mapper;

   }

}

This is the DateHolder class.

import java.util.HashMap;
import java.util.Map;

import org.joda.time.DateTime;
import org.joda.time.LocalDateTime;

/**
 * This class defines dates whose variable names match the keys in my map.
 */
public class DateTimeHolder {

   private DateTime now;
   private LocalDateTime nowLocal;
   private LocalDateTime notNowLocal;

   public Map<String, Object> buildMap() {
      final Map<String, Object> dateMap = new HashMap<>();
      dateMap.put("now", now);
      dateMap.put("nowLocal", nowLocal);
      dateMap.put("notNowLocal", notNowLocal);
      return dateMap;
   }

   public DateTime getNow() {
      return now;
   }

   public void setNow(final DateTime now) {
      this.now = now;
   }

   public LocalDateTime getNowLocal() {
      return nowLocal;
   }

   public void setNowLocal(final LocalDateTime nowLocal) {
      this.nowLocal = nowLocal;
   }

   public LocalDateTime getNotNowLocal() {
      return notNowLocal;
   }

   public void setNotNowLocal(final LocalDateTime notNowLocal) {
      this.notNowLocal = notNowLocal;
   }

}

The output of running this code is below.

Starting map.
   key           value class                    value
   ---           -----------                    -----
   now           org.joda.time.DateTime         2018-05-05T06:58:52.104Z
   notNowLocal   org.joda.time.LocalDateTime    2007-03-25T02:30:00.000
   nowLocal      org.joda.time.LocalDateTime    2018-05-05T16:58:52.260

Map serialised to a JSON string.
   {"now":"2018-05-05T06:58:52.104Z","notNowLocal":"2007-03-25T02:30:00.000","nowLocal":"2018-05-05T16:58:52.260"}

Map de-serialised from JSON.
   key           value class                    value
   ---           -----------                    -----
   now           org.joda.time.DateTime         2018-05-05T06:58:52.104Z
   notNowLocal   org.joda.time.LocalDateTime    2007-03-25T02:30:00.000
   nowLocal      org.joda.time.LocalDateTime    2018-05-05T16:58:52.260

Maps are equal: true

This is great, and works. However, if I am going to write a custom class for this, it is likely that I don't need a map at all. I should just use DateHolder everywhere.

The vast majority of times I use Jackson it is for just this case: I want to serialise a custom class to JSON and back again (not a map). This technique is preferable particularly when you have objects with more than a few fields of multiple types, and even more so when you are talking about compound objects - when your variables are other custom classes and so on.

To top of post.

Map of String to multiple date types using a custom de-serialiser

The whole reason I started looking into this was because I had a situation with the following characteristics.

  • Lots of maps with different sets of keys.
  • The values are only strings or dates.
  • The keys used for dates are always the same, e.g. a key of abc will always map to a DateTime, whereas a key of xyz will always map to a LocalDateTime.
  • I didn't need the whole map in my application, just a few specific fields that were common across all the maps.

All of this means is that if I wrote a custom class, the only use it would have would be for translation purposes and would not be used anywhere else. It felt like it would make more sense to see if I could provided a better way to tell Jackson how to translate my maps.

One way to do this is to provide a custom de-serialiser. Here is the class declaration and the single method I need to override.

public class JodaMapDeserialiser extends StdDeserializer<Object> {

   @Override
   public Object deserialize(final JsonParser p,
         final DeserializationContext ctxt)
         throws IOException, JsonProcessingException {
   }

}

The first step is to create a way to translate my keys in such a way that I can tell what type of Jodatime a specific key would map to (otherwise I treat the value as a string).

/** Mapping between keys in the map to a type of Joda time. */
static 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);
   }
}

Here is some code to briefly show how this enum works.

System.out.printf("%s: %s.%n", "now", DateType.forKeyString("now"));
System.out.printf("%s: %s.%n", "nowLocal", DateType.forKeyString("nowLocal"));
System.out.printf("%s: %s.%n", "notNowLocal", DateType.forKeyString("notNowLocal"));
System.out.printf("%s: %s.%n", "anythingElse", DateType.forKeyString("anythingElse"));

The output from the above code is below.

now: DATE_TIME.
nowLocal: LOCAL_DATE_TIME.
notNowLocal: LOCAL_DATE_TIME.
anythingElse: null.

Now assume I have a method that gets the key and value from every map entry. Here is how I can translate the value into a Jodatime date or leave the value as a string, all depending on the key.

// Each entry in the map has a key and value.
final String value;     // Actual code has
final String key;       // values for these two.

// 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;
}

Again, I create a map with dates in it.

// Create 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);

// Put them into a map.
final Map<String, Object> dateMap = new HashMap<>();
dateMap.put("now", now);
dateMap.put("nowLocal", nowLocal);
dateMap.put("notNowLocal", notNowLocal);

This time, the mapper is more complicated because I have to register my custom de-serialiser module.

final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JodaModule());
mapper.configure(WRITE_DATES_AS_TIMESTAMPS, false);

final SimpleModule dateDeserializerModule = new SimpleModule();
dateDeserializerModule.addDeserializer(Object.class,
      new JodaMapDeserialiser());
mapper.registerModule(dateDeserializerModule);

The code to serialise and then de-serialise the map looks quite familiar. Note that I am still creating a type definition to tell Jackson I want back a HashMap with String keys and Object values.

// 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 Map<String, Object> dateMapFromJson =
      mapper.readValue(dateMapJson, mapType);

Here is the complete example. First is the de-serialiser.

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. */
   static 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;
      }

   }

   /**
    * Briefly test this enum.
    *
    * @param args
    *           not used.
    */
   public static void main(final String[] args) {
      System.out.printf("%s: %s.%n", "now", DateType.forKeyString("now"));
      System.out.printf("%s: %s.%n", "nowLocal",
            DateType.forKeyString("nowLocal"));
      System.out.printf("%s: %s.%n", "notNowLocal",
            DateType.forKeyString("notNowLocal"));
      System.out.printf("%s: %s.%n", "anythingElse",
            DateType.forKeyString("anythingElse"));
   }

}

And the class that creates the map and does serialisation etc.

import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS;
import static java.lang.String.format;

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;

/**
 * Test converting a map of multiple date types to a JSON string and back.
 */
public class JodaTimeMapTestMultipleDateTypes {

   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 Map<String, Object> dateMapFromJson =
            mapper.readValue(dateMapJson, mapType);

      // Print off the maps, JSON string and proof that the maps are equal.
      System.out.printf("Starting map.%s%n%n", mapToString(dateMap));
      System.out.printf("Map serialised to a JSON string.%n   %s%n%n",
            dateMapJson);
      System.out.printf("Map de-serialised from JSON.%s%n%n",
            mapToString(dateMapFromJson));
      System.out.printf("Maps are equal: %s%n",
            dateMap.equals(dateMapFromJson));
   }

   /**
    * @param map
    *           with strings and dates.
    * @return string that tells me what the keys, value classes and values are.
    */
   private static String mapToString(final Map<String, Object> map) {
      return format("%n   %-13s %-30s %s", "key", "value class", "value") //
            + format("%n   %-13s %-30s %s", "---", "-----------", "-----") //
            + map.entrySet().stream().map(entry -> {
               final Object value = entry.getValue();
               return format("%n   %-13s %-30s %s", entry.getKey(),
                     value.getClass().getName(), value);
            }).collect(Collectors.joining());
   }

   /**
    * @return object that transform my maps to strings and back.
    */
   private static ObjectMapper mapper() {
      final ObjectMapper mapper = new ObjectMapper();
      mapper.registerModule(new JodaModule());
      mapper.configure(WRITE_DATES_AS_TIMESTAMPS, false);

      final SimpleModule dateDeserializerModule = new SimpleModule();
      dateDeserializerModule.addDeserializer(Object.class,
            new JodaMapDeserialiser());
      mapper.registerModule(dateDeserializerModule);

      return mapper;

   }

}

The output from running JodaTimeMapTestMultipleDateTypes is below.

Starting map.
   key           value class                    value
   ---           -----------                    -----
   now           org.joda.time.DateTime         2018-05-05T08:01:39.033Z
   notNowLocal   org.joda.time.LocalDateTime    2007-03-25T02:30:00.000
   nowLocal      org.joda.time.LocalDateTime    2018-05-05T18:01:39.204

Map serialised to a JSON string.
   {"now":"2018-05-05T08:01:39.033Z","notNowLocal":"2007-03-25T02:30:00.000","nowLocal":"2018-05-05T18:01:39.204"}

Map de-serialised from JSON.
   key           value class                    value
   ---           -----------                    -----
   now           org.joda.time.DateTime         2018-05-05T08:01:39.033Z
   notNowLocal   org.joda.time.LocalDateTime    2007-03-25T02:30:00.000
   nowLocal      org.joda.time.LocalDateTime    2018-05-05T18:01:39.204

Maps are equal: true

To top of post.

Maven dependencies

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>

To top of post.

Updates to post.

  • Monday, 7th of May 2018, 09:58:20 AM. Added a TOC.