简体   繁体   中英

Deserialising string to map with multiple types using jackson

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: Hashmap with String keys and DateTime values.
  • 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 .

Your date objects are serialized as string thanks to the Jodamodule that you registered: "now":"2018-05-04T11:42:15.454Z"

When you deseriallize the Json string you expect a HashMap with String keys and Object values. How would Jackson know that those objects should be different type of dates, it sees only strings..?

What you could do is to create a custom deserializer for this and implement the logic to deserialize each date correctly (for example you could determine the type by regex).

public class MyDateDeserializer extends StdDeserializer<Object> {
    public MyDateDeserializer() {
        super(Object.class);
    }
    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        return convertStringToTheProperDate(p.readValueAs(String.class));
    }
    private Object convertStringToTheProperDate(String dateAsString) {
       // implement the logic to convert the string to the proper type
       return null;
    }
}

And then register the deserializer:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM