Перейти к основному содержимому

Дата в Jackson

· 10 мин. чтения

1. Обзор

В этом уроке мы сериализуем даты с Джексоном. Мы начнем с сериализации простого файла java.util. Date , затем Joda-Time и, наконец, Java 8 DateTime .

2. Сериализация даты в метку времени

Во-первых, давайте посмотрим, как сериализовать простой java.util.Date с помощью Jackson .

В следующем примере мы сериализуем экземпляр « Event », который имеет поле даты « eventDate »:

@Test
public void whenSerializingDateWithJackson_thenSerializedToTimestamp()
throws JsonProcessingException, ParseException {

SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm");
df.setTimeZone(TimeZone.getTimeZone("UTC"));

Date date = df.parse("01-01-1970 01:00");
Event event = new Event("party", date);

ObjectMapper mapper = new ObjectMapper();
mapper.writeValueAsString(event);
}

Важно отметить, что по умолчанию Джексон сериализует дату в формате метки времени (количество миллисекунд с 1 января 1970 года, UTC).

Фактический результат сериализации « event »:

{
"name":"party",
"eventDate":3600000
}

3. Сериализация даты в ISO-8601

Сериализация в этот краткий формат метки времени не оптимальна. Вместо этого давайте сериализуем дату в формате ISO-8601 :

@Test
public void whenSerializingDateToISO8601_thenSerializedToText()
throws JsonProcessingException, ParseException {

SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm");
df.setTimeZone(TimeZone.getTimeZone("UTC"));

String toParse = "01-01-1970 02:30";
Date date = df.parse(toParse);
Event event = new Event("party", date);

ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// StdDateFormat is ISO8601 since jackson 2.9
mapper.setDateFormat(new StdDateFormat().withColonInTimeZone(true));
String result = mapper.writeValueAsString(event);
assertThat(result, containsString("1970-01-01T02:30:00.000+00:00"));
}

Мы видим, что представление даты стало намного более читабельным.

4. Настройте формат даты ObjectMapper ``

В предыдущих решениях по-прежнему отсутствует полная гибкость выбора точного формата для представления экземпляров java.util.Date .

И наоборот, давайте посмотрим на конфигурацию, которая позволит нам установить наши форматы для представления дат :

@Test
public void whenSettingObjectMapperDateFormat_thenCorrect()
throws JsonProcessingException, ParseException {

SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm");

String toParse = "20-12-2014 02:30";
Date date = df.parse(toParse);
Event event = new Event("party", date);

ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(df);

String result = mapper.writeValueAsString(event);
assertThat(result, containsString(toParse));
}

Обратите внимание, что хотя мы теперь более гибки в отношении формата даты, мы по-прежнему используем глобальную конфигурацию на уровне всего ObjectMapper .

5. Используйте @JsonFormat для форматирования даты

Далее давайте рассмотрим аннотацию @JsonFormat для управления форматом даты в отдельных классах, а не глобально для всего приложения:

public class Event {
public String name;

@JsonFormat
(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
public Date eventDate;
}

Теперь давайте проверим это:

@Test
public void whenUsingJsonFormatAnnotationToFormatDate_thenCorrect()
throws JsonProcessingException, ParseException {

SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("UTC"));

String toParse = "20-12-2014 02:30:00";
Date date = df.parse(toParse);
Event event = new Event("party", date);

ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(event);
assertThat(result, containsString(toParse));
}

6. Пользовательский сериализатор даты

Затем, чтобы получить полный контроль над выводом, мы будем использовать пользовательский сериализатор для дат:

public class CustomDateSerializer extends StdSerializer<Date> {

private SimpleDateFormat formatter
= new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");

public CustomDateSerializer() {
this(null);
}

public CustomDateSerializer(Class t) {
super(t);
}

@Override
public void serialize (Date value, JsonGenerator gen, SerializerProvider arg2)
throws IOException, JsonProcessingException {
gen.writeString(formatter.format(value));
}
}

Теперь мы будем использовать его как сериализатор нашего поля « eventDate »:

public class Event {
public String name;

@JsonSerialize(using = CustomDateSerializer.class)
public Date eventDate;
}

Наконец, мы проверим это:

@Test
public void whenUsingCustomDateSerializer_thenCorrect()
throws JsonProcessingException, ParseException {

SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");

String toParse = "20-12-2014 02:30:00";
Date date = df.parse(toParse);
Event event = new Event("party", date);

ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(event);
assertThat(result, containsString(toParse));
}

7. Сериализировать Joda-Time с Джексоном

Даты не всегда являются экземпляром java.util.Date. На самом деле все больше и больше даты представлены каким-то другим классом, и распространенной является реализация DateTime из библиотеки Joda-Time.

Давайте посмотрим, как мы можем сериализовать DateTime с помощью Jackson .

Мы будем использовать модуль jackson-datatype-joda для готовой поддержки Joda-Time: ``

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.9.7</version>
</dependency>

Затем мы можем просто зарегистрировать JodaModule и готово:

@Test
public void whenSerializingJodaTime_thenCorrect()
throws JsonProcessingException {
DateTime date = new DateTime(2014, 12, 20, 2, 30,
DateTimeZone.forID("Europe/London"));

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JodaModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

String result = mapper.writeValueAsString(date);
assertThat(result, containsString("2014-12-20T02:30:00.000Z"));
}

8. Сериализация Joda DateTime с помощью пользовательского сериализатора

Если нам не нужна дополнительная зависимость Joda-Time Jackson, мы также можем использовать собственный сериализатор (аналогичный предыдущим примерам) для чистой сериализации экземпляров DateTime :

public class CustomDateTimeSerializer extends StdSerializer<DateTime> {

private static DateTimeFormatter formatter =
DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");

public CustomDateTimeSerializer() {
this(null);
}

public CustomDateTimeSerializer(Class<DateTime> t) {
super(t);
}

@Override
public void serialize
(DateTime value, JsonGenerator gen, SerializerProvider arg2)
throws IOException, JsonProcessingException {
gen.writeString(formatter.print(value));
}
}

Затем мы можем использовать его как сериализатор нашего свойства « eventDate »:

public class Event {
public String name;

@JsonSerialize(using = CustomDateTimeSerializer.class)
public DateTime eventDate;
}

Наконец, мы можем собрать все вместе и протестировать:

@Test
public void whenSerializingJodaTimeWithJackson_thenCorrect()
throws JsonProcessingException {

DateTime date = new DateTime(2014, 12, 20, 2, 30);
Event event = new Event("party", date);

ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(event);
assertThat(result, containsString("2014-12-20 02:30"));
}

9. Сериализуйте свидание Java 8 с Джексоном

Теперь давайте посмотрим, как сериализовать Java 8 DateTime, в этом примере LocalDateTime, используя Jackson . Мы можем использовать модуль jackson-datatype-jsr310 : ``

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.7</version>
</dependency>

Затем все, что нам нужно сделать, это зарегистрировать JavaTimeModule ( JSR310Module устарел), а Джексон позаботится обо всем остальном:

@Test
public void whenSerializingJava8Date_thenCorrect()
throws JsonProcessingException {
LocalDateTime date = LocalDateTime.of(2014, 12, 20, 2, 30);

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

String result = mapper.writeValueAsString(date);
assertThat(result, containsString("2014-12-20T02:30"));
}

10. Сериализация даты Java 8 без каких-либо дополнительных зависимостей

Если нам не нужна дополнительная зависимость, мы всегда можем использовать собственный сериализатор для записи даты и времени Java 8 в JSON :

public class CustomLocalDateTimeSerializer 
extends StdSerializer<LocalDateTime> {

private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

public CustomLocalDateTimeSerializer() {
this(null);
}

public CustomLocalDateTimeSerializer(Class<LocalDateTime> t) {
super(t);
}

@Override
public void serialize(
LocalDateTime value,
JsonGenerator gen,
SerializerProvider arg2)
throws IOException, JsonProcessingException {

gen.writeString(formatter.format(value));
}
}

Затем мы будем использовать сериализатор для нашего поля « eventDate »:

public class Event {
public String name;

@JsonSerialize(using = CustomLocalDateTimeSerializer.class)
public LocalDateTime eventDate;
}

Наконец, мы проверим это:

@Test
public void whenSerializingJava8DateWithCustomSerializer_thenCorrect()
throws JsonProcessingException {

LocalDateTime date = LocalDateTime.of(2014, 12, 20, 2, 30);
Event event = new Event("party", date);

ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(event);
assertThat(result, containsString("2014-12-20 02:30"));
}

11. Десериализовать дату

Теперь давайте посмотрим, как мы можем десериализовать Date с помощью Jackson . В следующем примере мы десериализуем экземпляр Event , содержащий дату:

@Test
public void whenDeserializingDateWithJackson_thenCorrect()
throws JsonProcessingException, IOException {

String json = "{"name":"party","eventDate":"20-12-2014 02:30:00"}";

SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(df);

Event event = mapper.readerFor(Event.class).readValue(json);
assertEquals("20-12-2014 02:30:00", df.format(event.eventDate));
}

12. Десериализуйте Joda ZonedDateTime с сохранением часового пояса

В своей конфигурации по умолчанию Джексон настраивает часовой пояс Joda ZonedDateTime на часовой пояс локального контекста. Поскольку часовой пояс локального контекста не установлен по умолчанию и должен быть настроен вручную, Джексон настраивает часовой пояс на GMT:

@Test
public void whenDeserialisingZonedDateTimeWithDefaults_thenNotCorrect()
throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Europe/Berlin"));
String converted = objectMapper.writeValueAsString(now);

ZonedDateTime restored = objectMapper.readValue(converted, ZonedDateTime.class);
System.out.println("serialized: " + now);
System.out.println("restored: " + restored);
assertThat(now, is(restored));
}

Тестовый пример завершится ошибкой с выводом:

serialized: 2017-08-14T13:52:22.071+02:00[Europe/Berlin]
restored: 2017-08-14T11:52:22.071Z[UTC]

К счастью, есть быстрое и простое решение для этого странного поведения по умолчанию; мы просто должны сказать Джексону, чтобы он не менял часовой пояс.

Это можно сделать, добавив приведенную ниже строку кода в приведенный выше тестовый пример:

objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

Обратите внимание, что для сохранения часового пояса мы также должны отключить поведение по умолчанию сериализации даты в метку времени.

13. Пользовательский десериализатор даты

Мы также можем использовать собственный десериализатор даты . Мы напишем собственный десериализатор для свойства « eventDate »:

public class CustomDateDeserializer extends StdDeserializer<Date> {

private SimpleDateFormat formatter =
new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");

public CustomDateDeserializer() {
this(null);
}

public CustomDateDeserializer(Class<?> vc) {
super(vc);
}

@Override
public Date deserialize(JsonParser jsonparser, DeserializationContext context)
throws IOException, JsonProcessingException {
String date = jsonparser.getText();
try {
return formatter.parse(date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}

Далее мы будем использовать его как десериализатор « eventDate »:

public class Event {
public String name;

@JsonDeserialize(using = CustomDateDeserializer.class)
public Date eventDate;
}

Наконец, мы проверим это:

@Test
public void whenDeserializingDateUsingCustomDeserializer_thenCorrect()
throws JsonProcessingException, IOException {

String json = "{"name":"party","eventDate":"20-12-2014 02:30:00"}";

SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
ObjectMapper mapper = new ObjectMapper();

Event event = mapper.readerFor(Event.class).readValue(json);
assertEquals("20-12-2014 02:30:00", df.format(event.eventDate));
}

14. Исправление исключения InvalidDefinition ``

При создании экземпляра LocalDate мы можем столкнуться с исключением:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance
of `java.time.LocalDate`(no Creators, like default construct, exist): no String-argument
constructor/factory method to deserialize from String value ('2014-12-20') at [Source:
(String)"2014-12-20"; line: 1, column: 1]

Эта проблема возникает из-за того, что JSON изначально не имеет формата даты, поэтому даты представляются как String .

Строковое представление даты отличается от объекта типа LocalDate в памяти, поэтому нам нужен внешний десериализатор для чтения этого поля из String и сериализатор для преобразования даты в формат String . ``

Эти методы также применяются к LocalDateTime, единственное изменение заключается в использовании эквивалентного класса для LocalDateTime .

14.1. Джексон зависимость

Джексон позволяет нам исправить это несколькими способами. Во-первых, мы должны убедиться, что зависимость jsr310 есть в нашем pom.xml :

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.11.0</version>
</dependency>

14.2. Сериализация к одному объекту даты

Чтобы иметь возможность обрабатывать LocalDate , нам нужно зарегистрировать JavaTimeModule с помощью нашего ObjectMapper .

Нам также нужно отключить функцию WRITE_DATES_AS_TIMESTAMPS в ObjectMapper , чтобы Джексон не добавлял цифры времени в вывод JSON:

@Test
public void whenSerializingJava8DateAndReadingValue_thenCorrect() throws IOException {
String stringDate = "\"2014-12-20\"";

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

LocalDate result = mapper.readValue(stringDate, LocalDate.class);
assertThat(result.toString(), containsString("2014-12-20"));
}

Здесь мы использовали нативную поддержку Джексона для сериализации и десериализации дат.

14.3. Аннотация в POJO

Другой способ решить проблему — использовать аннотации LocalDateDeserializer и JsonFormat на уровне объекта:

public class EventWithLocalDate {

@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
public LocalDate eventDate;
}

Аннотация @JsonDeserialize используется для указания пользовательского десериализатора для демаршалирования объекта JSON. Точно так же @JsonSerialize указывает пользовательский сериализатор, который следует использовать при маршалинге сущности.

Кроме того, аннотация @JsonFormat позволяет указать формат, в котором мы будем сериализовать значения даты. Следовательно, этот POJO можно использовать для чтения и записи JSON:

@Test
public void whenSerializingJava8DateAndReadingFromEntity_thenCorrect() throws IOException {
String json = "{\"name\":\"party\",\"eventDate\":\"20-12-2014\"}";

ObjectMapper mapper = new ObjectMapper();

EventWithLocalDate result = mapper.readValue(json, EventWithLocalDate.class);
assertThat(result.getEventDate().toString(), containsString("2014-12-20"));
}

Хотя этот подход требует больше работы, чем использование значений по умолчанию JavaTimeModule , он гораздо более настраиваемый.

15. Заключение

В этой обширной статье о датах мы рассмотрели несколько способов, которыми Джексон может помочь маршалировать и демаршалировать дату в JSON , используя разумный формат, который мы можем контролировать. ****

Как всегда, код примеров можно найти на GitHub .