Jackson Date Serialize + Deserialize


The Problem
By default, Jackson writes(serialize) date as time stamp number, deserialize from time stamp number and the formats Jackson supported to Date.
SerializationFeature
StdDateFormat - data formats Jackson supports
public final static String DATE_FORMAT_STR_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
"yyyy-MM-dd'T'HH:mm:ss.SSSZ" 2001-07-04T12:08:56.235-0700
'Z' as an alias for "+0000"
protected final static String DATE_FORMAT_STR_ISO8601_Z = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
protected final static String DATE_FORMAT_STR_PLAIN = "yyyy-MM-dd";
protected final static String DATE_FORMAT_STR_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";

But in some cases, we want to serialize date as a more readable string, deserialize from the date string not supported by Jackson by default, and be more lenient.

The Solution
To serialize date as a more readable string, we can configure Jackson's ObjectMapper:
final DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
objectMapper().setDateFormat(sdf);
To support more date string format, we can create CustomDateDeserializer, and try all formats until one passes.

Jackson's supports parse timestamp number and some date formatted string to date object.
We extend its function to add more supported date format, such as "yyyy-MM-dd"(only date part)

Our CustomDateDeserializer first tries to parse the text as time stamp number, if failed, try all supported date formats until one passes. 

-- As our project uses Solr, I reuse org.apache.solr.common.util.DateUtil, and try all date formats that Jackson and Solr DateUtil support.


public class CustomDateDeserializer extends JsonDeserializer<Date> {
    public static final Collection<String> DEFAULT_DATE_FORMATS = new ArrayList<>();
    static {
        DEFAULT_DATE_FORMATS.addAll(Arrays.asList(JacksonStdDateFormat.getAllFormats()));
        DEFAULT_DATE_FORMATS.addAll(DateUtil.DEFAULT_DATE_FORMATS);
        // we can add other date formats
    }
    static class JacksonStdDateFormat extends StdDateFormat {
        private static final long serialVersionUID = 1L;

        public static String[] getAllFormats() {
            return ALL_FORMATS;
        }
    }
    @Override
    public Date deserialize(final JsonParser jsonparser, final DeserializationContext context) throws IOException {
        final String dateString = jsonparser.getText();
        try {
            return new Date(Long.valueOf(dateString));
        } catch (final Exception e1) {
            return tryParseAsDateString(dateString);
        }
    }

    protected static Date tryParseAsDateString(final String dateString) {
        try {
            // basically DateUtil tries every formats until one passes.
            return DateUtil.parseDate(dateString, DEFAULT_DATE_FORMATS);
        } catch (final ParseException e2) {
            throw new RuntimeException(MessageFormat.format("Unable to parse {0}", dateString));
        }
    }
}

// register CustomDateDeserializer
final SimpleModule dateModule = new SimpleModule().addDeserializer(Date.class, new CustomDateDeserializer());
objectMapper.registerModule(dateModule);
The Complete Code

public void testJacksonDate() throws IOException, ParseException {
    final DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));

    ObjectMapper objectMapper = new ObjectMapper();
    final Model model = new Model();
    final String date = "2016-03-12T20:00:00Z";
    model.setDate(sdf.parse(date));
    String json = objectMapper.writeValueAsString(model);
    // {"date":1457812800000}
    // objectMapper write Date as timestamp number
    System.out.println(json);
    System.out.println(objectMapper.readValue(json, Model.class));

    // https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com
    /// fasterxml/jackson/databind/util/StdDateFormat.java
    // Be default it can parse string formatted like below:

    // public final static String DATE_FORMAT_STR_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
    // https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html
    // "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 2001-07-04T12:08:56.235-0700
    // 'Z' as an alias for "+0000"
    // protected final static String DATE_FORMAT_STR_ISO8601_Z = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
    // protected final static String DATE_FORMAT_STR_PLAIN = "yyyy-MM-dd";
    // protected final static String DATE_FORMAT_STR_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
    System.out.println(objectMapper.readValue("{\"date\":\"2016-03-12T20:00:00Z\"}", Model.class));
    System.out.println(objectMapper.readValue("{\"date\":\"2016-03-12T20:00:00.000-0700\"}", Model.class));
    System.out.println(objectMapper.readValue("{\"date\":\"2016-03-12\"}", Model.class));
    System.out.println(objectMapper.readValue("{\"date\":\"2016-03-12T20:00:00\"}", Model.class));

    System.out.println(objectMapper.readValue("{\"date\":\"2016-03-12T20:00:00Z\"}", Model.class));

    // but it doesn't support this:
    try {
        System.out.println(objectMapper.readValue("{\"date\":\"2016-03-12 20:00:00\"}", Model.class));
        fail();
    } catch (final IOException e) {
        e.printStackTrace();
    }

    objectMapper = new ObjectMapper().setDateFormat(sdf);
    // now it will rite date as string: {"date":"2016-03-12T20:00:00Z"}
    json = objectMapper.writeValueAsString(model);
    System.out.println(json);
    assertThat(json, containsString(date));

    // it will use our own date format to format date, and parse string formated date.
    System.out.println(objectMapper.readValue(json, Model.class));
    System.out.println(objectMapper.readValue("{\"date\":1457812800000}", Model.class));

    System.out.println(objectMapper.readValue("{\"date\":\"2016-03-12T20:00:00Z\"}", Model.class));

    // it will not use Jackson's StdDateFormat, so it can't parse the formats StdDateFormat
    // supported
    try {
        // this will fail, no time parts
        objectMapper.readValue("{\"date\":\"2016-03-12\"}", Model.class);
        fail();
    } catch (final IOException e) {
        e.printStackTrace();
    }

    try {
        // this will fail, as missing seconds part
        objectMapper.readValue("{\"date\":\"2016-03-12T20:00Z\"}", Model.class);
        fail();
    } catch (final IOException e) {
        e.printStackTrace();
    }

    try {
        // fail due to the unexpected millseconds part
        objectMapper.readValue("{\"date\":\"2016-03-12T20:00:00.000Z\"}", Model.class);
        fail();
    } catch (final IOException e) {
        e.printStackTrace();
    }

    // The server side wants to support be lenient, accept all different format date string
    // as we use Solr, so we create a CustomDateDeserializer that adds all date formats Jackson
    // supported and Solr Supported.
    final SimpleModule dateModule = new SimpleModule().addDeserializer(Date.class, new CustomDateDeserializer());
    objectMapper = new ObjectMapper().setDateFormat(sdf).registerModule(dateModule);

    // now all pass
    objectMapper.readValue("{\"date\":\"2016-03-12\"}", Model.class);
    objectMapper.readValue("{\"date\":\"2016-03-12T20:00Z\"}", Model.class);
    objectMapper.readValue("{\"date\":\"2016-03-12T20:00:00.000Z\"}", Model.class);
}

Read More
Jackson Essentials - the JSON Libaray
Using Jackson JSON View to Protect Mass Assignment Vulnerabilities
Merge JSON Objects: Jackson + BeanUtils.copyProperties
Jackson Generic Type + Java Type Erasure
Jackson Date Serialize + Deserialize

Labels

adsense (5) Algorithm (69) Algorithm Series (35) Android (7) ANT (6) bat (8) Big Data (7) Blogger (14) Bugs (6) Cache (5) Chrome (19) Code Example (29) Code Quality (7) Coding Skills (5) Database (7) Debug (16) Design (5) Dev Tips (63) Eclipse (32) Git (5) Google (33) Guava (7) How to (9) Http Client (8) IDE (7) Interview (88) J2EE (13) J2SE (49) Java (186) JavaScript (27) JSON (7) Learning code (9) Lesson Learned (6) Linux (26) Lucene-Solr (112) Mac (10) Maven (8) Network (9) Nutch2 (18) Performance (9) PowerShell (11) Problem Solving (11) Programmer Skills (6) regex (5) Scala (6) Security (9) Soft Skills (38) Spring (22) System Design (11) Testing (7) Text Mining (14) Tips (17) Tools (24) Troubleshooting (29) UIMA (9) Web Development (19) Windows (21) xml (5)