38

I want to serialize a Map with Jackson. The Date should be serialized as a timestamp, like all my other dates.

The following code renders the keys in the form "Tue Mar 11 00:00:00 CET 1952" (which is Date.toString()) instead of the timestamp.

Map<Date, String> myMap = new HashMap<Date, String>();
...
ObjectMapper.writeValue(myMap)

I assume this is because of type erasure and jackson doesn't know at runtime that the key is a Date. But I didn't find a way to pass a TypeReference to any writeValue method.

Is there a simple way to achieve my desired behaviour or are all keys always rendered as Strings by jackson?

Thanks for any hint.

Florian Gutmann
  • 2,666
  • 2
  • 20
  • 28
  • JSON maps are just JSON objects with element name/value pairs. A JSON element name must be a string. So, Jackson is generating a string from what it was provided. – Programmer Bruce Jul 04 '11 at 18:03

3 Answers3

49

The default map key serializer is StdKeySerializer, and it simply does this.

String keyStr = (value.getClass() == String.class) ? ((String) value) : value.toString();
jgen.writeFieldName(keyStr);

You could make use of the SimpleModule feature, and specify a custom key serializer, using the addKeySerializer method.


And here's how that could be done.

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ObjectWriter;
import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.map.type.MapType;
import org.codehaus.jackson.map.type.TypeFactory;

public class CustomKeySerializerDemo
{
  public static void main(String[] args) throws Exception
  {
    Map<Date, String> myMap = new HashMap<Date, String>();
    myMap.put(new Date(), "now");
    Thread.sleep(100);
    myMap.put(new Date(), "later");

    ObjectMapper mapper = new ObjectMapper();
    System.out.println(mapper.writeValueAsString(myMap));
    // {"Mon Jul 04 11:38:36 MST 2011":"now","Mon Jul 04 11:38:36 MST 2011":"later"}

    SimpleModule module =  
      new SimpleModule("MyMapKeySerializerModule",  
          new Version(1, 0, 0, null));
    module.addKeySerializer(Date.class, new DateAsTimestampSerializer());

    MapType myMapType = TypeFactory.defaultInstance().constructMapType(HashMap.class, Date.class, String.class);

    ObjectWriter writer = new ObjectMapper().withModule(module).typedWriter(myMapType);
    System.out.println(writer.writeValueAsString(myMap));
    // {"1309806289240":"later","1309806289140":"now"}
  }
}

class DateAsTimestampSerializer extends JsonSerializer<Date>
{
  @Override
  public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) 
      throws IOException, JsonProcessingException
  {
    jgen.writeFieldName(String.valueOf(value.getTime()));
  }
}

Update for the latest Jackson (2.0.4):

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;

public class CustomKeySerializerDemo
{
  public static void main(String[] args) throws Exception
  {
    Map<Date, String> myMap = new HashMap<Date, String>();
    myMap.put(new Date(), "now");
    Thread.sleep(100);
    myMap.put(new Date(), "later");

    ObjectMapper mapper = new ObjectMapper();
    System.out.println(mapper.writeValueAsString(myMap));
    // {"2012-07-13T21:14:09.499+0000":"now","2012-07-13T21:14:09.599+0000":"later"}

    SimpleModule module = new SimpleModule();
    module.addKeySerializer(Date.class, new DateAsTimestampSerializer());

    MapType myMapType = TypeFactory.defaultInstance().constructMapType(HashMap.class, Date.class, String.class);

    ObjectWriter writer = new ObjectMapper().registerModule(module).writerWithType(myMapType);
    System.out.println(writer.writeValueAsString(myMap));
    // {"1342214049499":"now","1342214049599":"later"}
  }
}

class DateAsTimestampSerializer extends JsonSerializer<Date>
{
  @Override
  public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) 
      throws IOException, JsonProcessingException
  {
    jgen.writeFieldName(String.valueOf(value.getTime()));
  }
}
informatik01
  • 16,038
  • 10
  • 74
  • 104
Programmer Bruce
  • 64,977
  • 7
  • 99
  • 97
  • Thanks for the answer, I thought about something like this. Anyways it would be overkill for my problem, so I just restructured my data a little. – Florian Gutmann Jul 04 '11 at 19:08
  • I find that addKeySerializer() doesn't seem to do what I expect. I am serializing an instance of `Map` and Jackson fails to use my Foo key serializer. – stickfigure Jan 24 '12 at 07:19
  • Nevermind, I hadn't realized that I can't use the same FooSerializer for key and value; key serializers must jgen.writeFieldName(). – stickfigure Jan 24 '12 at 07:29
  • 1
    And nevermind my nevermind... apparently jackson only respects key serializers when you have a class that extends the generic, like `class MyMap extends Map` or the Map is a field in another class. I understand why these situations are different (the metadata evades erasure) but I don't understand why Jackson isn't just looking at the runtime type of the key object. – stickfigure Jan 24 '12 at 07:34
  • Would this approach work with serializing a Map? – heyomi Feb 17 '13 at 21:22
5

As usual, Bruce's answer is right on the spot.

One additional thought is that since there is a global setting for serializing Date values as timestamps:

SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS

Maybe that should apply here as well. And/or at least use standard ISO-8601 format for text. The main practical issue there is that of backwards compatibility; however, I doubt that current use of plain toString() is very useful as it is neither efficient nor convenient (to read back the value).

So if you want, you might want to file a feature request; this sounds like sub-optimal handling of Map keys by Jackson.

informatik01
  • 16,038
  • 10
  • 74
  • 104
StaxMan
  • 113,358
  • 34
  • 211
  • 239
1

Since Jackson 2.0 (maybe 1.9, too), WRITE_DATE_KEYS_AS_TIMESTAMPS can be used to change this particular behavior.

Usage example for ObjectMapper:

ObjectMapper m = new ObjectMapper().configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, true);

and for ObjectWriter:

ObjectWriter w = mapper.with(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
Roben
  • 840
  • 9
  • 19