Jak ustawić format daty dla danych wejściowych w Jacksonie?

Jak ustawić format daty dla danych wejściowych w Jacksonie?
SI
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 70
0

Projekt we frameworku Quarkus.

Ponieważ daty z GUI przychodzą w formacie 2024-05-01T12:12:12 z kontrolki <input type="datetime-local"> a w back-endzie daty są przechowywane jako instancje klasy OffsetDateTime to przy mapowaniu takiej daty (bez offsetu) do OffsetDateTime pojawia się błąd.

Próbowałem sobie z tym poradzić, pisząc i podpinając konwerter:

Kopiuj

@Singleton
public class ObjectMapperConfig implements ObjectMapperCustomizer {

  // tutaj dodaję moduł do Jacksona, żeby mapował OffsetDateTime według własnego deserializera
  public void customize(ObjectMapper mapper) {
    var module = new SimpleModule();
    module.addDeserializer(OffsetDateTime.class, new CustomOffsetDateTimeDeserializer());
    mapper.registerModule(module);
  }

  // A tu jest własny deserializer
  static class CustomOffsetDateTimeDeserializer extends JsonDeserializer<OffsetDateTime> {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

    // Niestety do tej metody nawet nie wchodzi
    @Override
    public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
      String dateString = p.getText();
      try {
        return OffsetDateTime.parse(dateString);
      } catch (Exception e) {
        var localDateTime = LocalDateTime.parse(dateString, FORMATTER);
        return localDateTime.atOffset(ZoneOffset.systemDefault().getRules().getOffset(localDateTime));
      }
    }

  }

}

Niestety do tej metody nawet nie wchodzi.

Odpaliłem debugger i sprawdziłem, że moduł jest podpięty, ale Jackson do deserializacji używa InstantDeserializer zamiast CustomOffsetDateTimeDeserializer.

Czy macie inny pomysł, jak zmusić Jacksona, aby czytał daty w formacie yyyy-MM-dd'T'HH:mm:ss (ale równie dobrze może przyjść data w innym formacie)?

dalbajob
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 149
0

Nie znam się aż tak na mapowaniu z Jacksonem, ale widzę, że próbujesz zamienić LocalDateTime (abstrakcyjny date time bez strefy czasowej) na OffsetDateTime (konkretny date time z offsetem strefy czasowej) po prostu interpretując go jako date time w systemowej strefie.

Więc muszę zadać pytanie kontrolne:
Czy jesteś pewien że ten date time z GUI zawsze będzie datą w twojej systemowej strefie czasowej? Co jeśli np. backend postawisz na serwerze w USA i będzie tam jakaś amerykańska strefa czasowa ustawiona?

Czemu z GUI daty przychodzą bez strefy czasowej? Co oznacza ta data?

SI
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 70
0

Takie jest założenie, że w tym przypadku offset (strefę czasową) ustawiamy na serwerze. W GUI jest to wybór daty lokalnej bez uwzględniania strefy czasowej.

dalbajob
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 149
0
Shiba Inu napisał(a):

Takie jest założenie, że w tym przypadku offset (strefę czasową) ustawiamy na serwerze.

Nie znam twojej aplikacji, ale jak dla mnie to jest bardzo złe założenie. Zmiana strefy czasowej serwera zmieni interpretację dat z GUI, ale daty zapisane wcześniej nie zmienią się.

Shiba Inu napisał(a):

W GUI jest to wybór daty lokalnej bez uwzględniania strefy czasowej.

Ale co oznacza ta data w GUI? Datę odpalenia cronjoba, datę urodzenia co do sekundy, co to jest? Do czego dążę - czy ta data wybrana w GUI dla użytkownika tego GUI oznacza coś abstrakcyjnego, czy konkretny punkt w czasie?

SI
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 70
0

Data oznacza konkretny punkt w czasie, np. datę odpalenia cronjoba, datę wykonania jakiejś czynności. Wcześniej na serwerze były to daty bez strefy czasowej (LocalDateTime), a potem uznano, że lepiej jednak trzymać strefę czasową (OffsetDateTime), bo się strefy rozjeżdżały przy komunikacji z zewnętrznymi usługami. Nie jestem architektem tego projektu. Oczywiście z GUI można wysyłać timestamp lub datę z offsetem, tylko trzeba by użyć innych kontrolek (lub zamieniać ręcznie format daty), a po drugie nie wiadomo, w jakiej strefie czasowej użytkownikowi powinny się wyświetlać, kiedy faktycznie ktoś będzie miał w systemie inną strefę czasową. Jest to jednak zagadnienie biznesowe. Na chwilę obecną problem jest taki, że prawie wszędzie są kontrolki natywne do wyboru daty, a po zmianie klasy z LocalDateTime do OffsetDateTime wszędzie sypie błędami przy próbie przekazania jakiejkolwiek daty z GUI do back-endu.

Wiem, że można użyć adnotacji @JsonDeserialize na konkretnym polu, ale jest kolejny mankament, że klasy encyjne są generowane przez jsonschema2pojo. Powód? Żeby nie robić projektu common, tylko żeby każdy moduł sobie generował encje ze schemy. Wolę skonfigurować Jacksona, żeby czytał takie daty.

Black007
  • Rejestracja: dni
  • Ostatnio: dni
0

Hej, nie robiłem nic w quarkusie, ale w springboot mam tak:

Kopiuj
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.text.SimpleDateFormat;

@Configuration
public class RestApiConfiguration {

  @Bean
  public ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new JavaTimeModule());
    objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
    return objectMapper;
  }
}

JavaTimeModule ułatwia Ci życie (tam ma te wszytskie jrsy i inne bajery :D), a SimpleDateFormat powinien zadziałać z twoim formatem tj: 2024-05-01T12:12:12.
Daj znać czy działa

dalbajob
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 149
1
Shiba Inu napisał(a):

Na chwilę obecną problem jest taki, że prawie wszędzie są kontrolki natywne do wyboru daty, a po zmianie klasy z LocalDateTime do OffsetDateTime wszędzie sypie błędami przy próbie przekazania jakiejkolwiek daty z GUI do back-endu.

OK, to tylko zostawiam ostrzeżenie - "panie, to kiedyś j*bnie" - i już przestaję cię o to męczyć.

Jeśli chodzi o to dlaczego Jackson woła InstantDeserializer to wygląda na to, że po prostu nie ma customowej klasy i InstantDeserializer jest używany dla OffsetDateTime - https://github.com/FasterXML/jackson-modules-java8/issues/130#issuecomment-523172197.

Więc dla mnie to wygląda jakby twój deserializer wcale się nie dodawał poprawnie i używany jest defaultowy. Z tym niestety nie pomogę już.

Możesz spróbować setDateFormat jak wyżej lub - https://github.com/FasterXML/jackson-modules-java8/issues/130#issuecomment-1210140383 - chociaż jeśli to implicite używa systemowej strefy czasowej, to jest to jeszcze gorsze niż twoje explicite mapowanie.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
dalbajob napisał(a):
Shiba Inu napisał(a):

Na chwilę obecną problem jest taki, że prawie wszędzie są kontrolki natywne do wyboru daty, a po zmianie klasy z LocalDateTime do OffsetDateTime wszędzie sypie błędami przy próbie przekazania jakiejkolwiek daty z GUI do back-endu.

OK, to tylko zostawiam ostrzeżenie - "panie, to kiedyś j*bnie" - i już przestaję cię o to męczyć.

YAGNI.

To jest okej że @Shiba Inu inu nie próbuje teraz rozwiązać problemu który ma.

Ty proponujesz naprawienie problemu teraz, który potencjalnie wystąpi w przyszłości. Zagranie mądre, gdyby software należało napisać "raz a dobrze", i wtedy musisz podjąć same dobre decyzje na początku. Ale software powinien być łatwy w zmianie, więc jeśli problem wystąpi później, to później się go naprawi.

dalbajob
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 149
1
Riddle napisał(a):
dalbajob napisał(a):
Shiba Inu napisał(a):

Na chwilę obecną problem jest taki, że prawie wszędzie są kontrolki natywne do wyboru daty, a po zmianie klasy z LocalDateTime do OffsetDateTime wszędzie sypie błędami przy próbie przekazania jakiejkolwiek daty z GUI do back-endu.

OK, to tylko zostawiam ostrzeżenie - "panie, to kiedyś j*bnie" - i już przestaję cię o to męczyć.

YAGNI.

To jest okej że @Shiba Inu inu nie próbuje teraz rozwiązać problemu który ma.

Ty proponujesz naprawienie problemu teraz, który potencjalnie wystąpi w przyszłości. Zagranie mądre, gdyby software należało napisać "raz a dobrze", i wtedy musisz podjąć same dobre decyzje na początku. Ale software powinien być łatwy w zmianie, więc jeśli problem wystąpi później, to później się go naprawi.

Tutaj sytuacja po prostu wyglądała mi na Problem XY, gdzie złe zaprojektowanie formatu dat mogło być prawdziwym źródłem problemu, a bolączki z Jacksonem jedynie jego efektem ubocznym. Może zamiast siepać się z masowaniem własnych deserializatorów, lepiej po prostu zrobić to porządniej (jeśli się da).

Natomiast ogólnie zgadzam się z tą zasadą i nie uważam że soft musi być napisany raz a dobrze. Czasem trzeba po prostu zrobić żeby jako tako działało - tylko dobrze wtedy pamiętać, że "panie, to kiedyś j*bnie" :)

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
dalbajob napisał(a):

Czasem trzeba po prostu zrobić żeby jako tako działało - tylko dobrze wtedy pamiętać, że "panie, to kiedyś j*bnie" :)

Moim zdaniem nie trzeba pamiętać.

  • Jak problem nie wystąpi - to nie warto było nic robić.
  • Jak wystąpi, to nawet jak o nim nie będziesz pamiętał, to teraz go masz, więc i tak musisz naprawić, mając konkretny use-case w ręce.
SI
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 70
0
Black007 napisał(a):

SimpleDateFormat powinien zadziałać z twoim formatem tj: 2024-05-01T12:12:12.
Daj znać czy działa

Niestety nie działa.

Kopiuj
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"));

Zadziałał mixin

Kopiuj
  public void customize(ObjectMapper mapper) {
    mapper.addMixIn(OffsetDateTime.class, DateMixin.class);
  }

  @JsonDeserialize(converter = CustomOffsetDateTimeConverter.class)
  static class DateMixin {
  }

  static class CustomOffsetDateTimeConverter extends StdConverter<String, OffsetDateTime> {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

    @Override
    public OffsetDateTime convert(String dateString) {
      try {
        return OffsetDateTime.parse(dateString);
      } catch (Exception e) {
        try {
          var localDateTime = LocalDateTime.parse(dateString, FORMATTER);
          return localDateTime.atOffset(ZoneOffset.systemDefault().getRules().getOffset(localDateTime));
        } catch (Exception ex) {
          var instant = Instant.parse(dateString);
          return OffsetDateTime.ofInstant(instant, ZoneOffset.systemDefault());
        }
      }
    }
  }

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.