Как установить формат строки для java.time.Instant с помощью objectMapper?

У меня есть объект с java.time.Instant для созданного поля данных:

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class Item {
    private String id;
    private String url;
    private Instant createdDate;
}

Я использую com.fasterxml.jackson.databind.ObjectMapper чтобы сохранить элемент в Elasticsearch как JSON:

bulkRequestBody.append(objectMapper.writeValueAsString(item));

ObjectMapper сериализует это поле как объект:

"createdDate": {
    "epochSecond": 1502643595,
    "nano": 466000000
}

Я пытался аннотировать @JsonFormat(shape = JsonFormat.Shape.STRING) но это не работает для меня.

Мой вопрос в том, как я мог бы сериализовать это поле как строку 2010-05-30 22:15:52?

Ответ 1

Одним из решений является использование jackson-modules-java8. Затем вы можете добавить JavaTimeModule к вашему объекту mapper:

ObjectMapper objectMapper = new ObjectMapper();

JavaTimeModule module = new JavaTimeModule();
objectMapper.registerModule(module);

По умолчанию Instant сериализуется как значение эпохи (секунды и наносекунды в одном числе):

{"createdDate":1502713067.720000000}

Вы можете изменить это, установив в mapper:

objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

Это даст результат:

{"createdDate":"2017-08-14T12:17:47.720Z"}

Оба указанных выше формата десериализованы без какой-либо дополнительной настройки.

Чтобы изменить формат сериализации, просто добавьте аннотацию JsonFormat в поле:

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
private Instant createdDate;

Вам нужно установить часовой пояс, иначе Instant не может быть правильно сериализовано (он выдает исключение). Выход будет:

{"createdDate":"2017-08-14 12:17:47"}

Другая альтернатива, если вы не хотите (или не можете) использовать модули java8, заключается в создании пользовательского сериализатора и десериализатора с использованием java.time.format.DateTimeFormatter:

public class MyCustomSerializer extends JsonSerializer<Instant> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneOffset.UTC);

    @Override
    public void serialize(Instant value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
        String str = fmt.format(value);

        gen.writeString(str);
    }
}

public class MyCustomDeserializer extends JsonDeserializer<Instant> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneOffset.UTC);

    @Override
    public Instant deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        return Instant.from(fmt.parse(p.getText()));
    }
}

Затем вы аннотируете поле этими пользовательскими классами:

@JsonDeserialize(using = MyCustomDeserializer.class)
@JsonSerialize(using = MyCustomSerializer.class)
private Instant createdDate;

Выход будет:

{"createdDate":"2017-08-14 12:17:47"}

Одна деталь заключается в том, что в сериализованной строке вы отбрасываете долю секунды (все после десятичной точки). Таким образом, при десериализации эта информация не может быть восстановлена (она будет установлена на ноль).

В приведенном выше примере исходный Instant - это 2017-08-14T12:17:47.720Z, но сериализованная строка - 2017-08-14 12:17:47 (без доли секунды), поэтому, когда десериализованный результирующий Instant 2017-08-14T12:17:47Z (.720 миллисекунд).

Ответ 2

Для тех, кто ищет синтаксис Java 8. Вам нужна последняя версия jackson-datatype-jsr310 в вашем POM и зарегистрирован следующий модуль:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

Чтобы проверить этот код

@Test
void testSeliarization() throws IOException {
    String expectedJson = "{\"parseDate\":\"2018-12-04T18:47:38.927Z\"}";
    MyPojo pojo = new MyPojo(ZonedDateTime.parse("2018-12-04T18:47:38.927Z"));

    // serialization
    assertThat(objectMapper.writeValueAsString(pojo)).isEqualTo(expectedJson);

    // deserialization
    assertThat(objectMapper.readValue(expectedJson, MyPojo.class)).isEqualTo(pojo);
}

Ответ 3

Вы должны добавить ниже зависимость

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

Затем зарегистрируйте модули следующим образом:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();

Ответ 4

Вот какой-то кодлин Kotlin для форматирования Instant, поэтому он не содержит миллисекунд, вы можете использовать собственные форматеры даты

ObjectMapper().apply {
        val javaTimeModule = JavaTimeModule()
        javaTimeModule.addSerializer(Instant::class.java, Iso8601WithoutMillisInstantSerializer())
        registerModule(javaTimeModule)
        disable(WRITE_DATES_AS_TIMESTAMPS)
    }

private class Iso8601WithoutMillisInstantSerializer
        : InstantSerializer(InstantSerializer.INSTANCE, false, DateTimeFormatterBuilder().appendInstant(0).toFormatter())

Ответ 5

В моем случае достаточно было зарегистрировать JavaTimeModule:

  ObjectMapper objectMapper = new ObjectMapper();
  JavaTimeModule module = new JavaTimeModule();
  objectMapper.registerModule(module);

  messageObject = objectMapper.writeValueAsString(event);

В объекте события у меня есть поле типа Instant.

В десериализации вы также должны зарегистрировать модуль времени Java:

ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());

Event event = objectMapper.readValue(record.value(), Event.class);

Ответ 6

Вы можете использовать Spring ObjectMapper, который уже настроен с JavaTimeModule. Просто вставьте его из контекста Spring и не используйте new ObjectMapper().