Format daty Mapowanie do JSON Jackson

154

Mam format daty pochodzący z API w następujący sposób:

"start_time": "2015-10-1 3:00 PM GMT+1:00"

Który jest RRRR-DD-MM HH: MM Am / pm GMT timestamp. Mapuję tę wartość do zmiennej Date w POJO. Oczywiście pokazuje błąd konwersji.

Chciałbym wiedzieć 2 rzeczy:

  1. Jakiego formatowania potrzebuję, aby przeprowadzić konwersję w Jackson? Czy data jest dobrym typem pola do tego celu?
  2. Ogólnie rzecz biorąc, czy istnieje sposób na przetworzenie zmiennych, zanim zostaną zmapowane do elementów Object przez Jacksona? Coś jak zmiana formatu, obliczenia itp.
Kevin Rave
źródło
To jest naprawdę dobry przykład, umieść adnotację w polu klasy: java.dzone.com/articles/how-serialize-javautildate
digz6666
Teraz mają stronę wiki do obsługi dat: wiki.fasterxml.com/JacksonFAQDateHandling
Sutra
W dzisiejszych czasach nie powinieneś już używać tej Dateklasy. java.time, nowoczesny interfejs API daty i czasu Javy, zastąpił go prawie 5 lat temu. Użyj go i FasterXML / jackson-modules-java8 .
Ole VV

Odpowiedzi:

124

Jakiego formatowania potrzebuję, aby przeprowadzić konwersję w Jackson? Czy data jest dobrym typem pola do tego celu?

Datejest do tego dobrym typem pola. Możesz łatwo przeprowadzić analizę JSON, używając ObjectMapper.setDateFormat:

DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm a z");
myObjectMapper.setDateFormat(df);

Ogólnie rzecz biorąc, czy istnieje sposób na przetworzenie zmiennych, zanim zostaną zmapowane do elementów Object przez Jacksona? Coś jak zmiana formatu, obliczenia itp.

Tak. Masz kilka opcji, w tym zaimplementowanie niestandardowego JsonDeserializer, np JsonDeserializer<Date>. Rozszerzenie . To dobry początek.

pb2q
źródło
2
Format 12-godzinny jest lepszy, jeśli format zawiera również oznaczenie AM / PM: DateFormat df = new SimpleDateFormat ("rrrr-MM-dd gg: mm a z");
John Scattergood,
Wypróbowałem wszystkie te rozwiązania, ale nie byłem w stanie zapisać zmiennej Date mojego POJO w wartości klucza mapy, również jako Data. Chcę, aby to następnie utworzyło wystąpienie BasicDbObject (API MongoDB) z mapy i konsekwentnie przechowywało zmienną w kolekcji bazy danych MongoDB jako Data (nie jako Long lub String). Czy to w ogóle możliwe? Dziękuję
RedEagle
1
Czy korzystanie z Javy 8 jest równie łatwe, LocalDateTimeczy ZonedDateTimezamiast Date? Ponieważ Datejest zasadniczo przestarzały (lub przynajmniej wiele jego metod), chciałbym użyć tych alternatyw.
houcros
Elementy javadocs dla setSateFormat () mówią, że to wywołanie sprawia, że ​​ObjectMapper nie jest już bezpieczny dla wątków. Stworzyłem klasy JsonSerializer i JsonDeserializer.
MiguelMunoz,
Ponieważ pytanie nie mówi java.util.Datewyraźnie, chcę zaznaczyć, że to nie działa w przypadku java.sql.Date.Zobacz także moją odpowiedź poniżej.
mosiężna małpa
329

Od wersji Jackson 2.0 możesz używać adnotacji @JsonFormat bezpośrednio na członkach Object;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm a z")
private Date date;
Olivier Lecrivain
źródło
61
Jeśli chcesz uwzględnić strefę czasową:@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
realPK
Cześć, który słoik ma tę adnotację. Używam wersji jackson-mapper-asl 1.9.10. Nie rozumiem
krishna Ram
1
@Ramki: jackson-annotations> = 2.0
Olivier Lecrivain
3
Ta adnotacja działa idealnie tylko w fazie serializacji, ale podczas deserializacji informacje o strefie czasowej i lokalizacji nie są w ogóle używane. Wypróbowałem strefę czasową = "CET" i strefę czasową "Europa / Budapeszt" z locale = "hu", ale żadna z nich nie działa i powoduje dziwne rozmowy w Kalendarzu. Tylko niestandardowa serializacja z deserializacją działała dla mnie, obsługując strefę czasową zgodnie z wymaganiami. Oto doskonały samouczek, jak korzystać z baeldung.com/jackson-serialize-dates
Miklos Krivan
1
To jest osobny projekt jar o nazwie Jackson Annotations . Przykładowy wpis pom
bub
52

Oczywiście istnieje zautomatyzowany sposób zwany serializacją i deserializacją i można go zdefiniować za pomocą określonych adnotacji ( @JsonSerialize , @JsonDeserialize ), o czym również wspomina pb2q.

Możesz użyć zarówno java.util.Date, jak i java.util.Calendar ... i prawdopodobnie również JodaTime.

Adnotacje @JsonFormat nie działały tak, jak chciałem ( dostosowały strefę czasową do innej wartości) podczas deserializacji (serializacja działała idealnie):

@JsonFormat(locale = "hu", shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "CET")

@JsonFormat(locale = "hu", shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Europe/Budapest")

Jeśli chcesz przewidzieć wynik, musisz użyć niestandardowego serializatora i niestandardowego deserializatora zamiast adnotacji @JsonFormat. Znalazłem naprawdę dobry samouczek i rozwiązanie tutaj http://www.baeldung.com/jackson-serialize-dates

Istnieją przykłady pól daty, ale potrzebowałem pól kalendarza, więc oto moja implementacja :

Serializer klasa:

public class CustomCalendarSerializer extends JsonSerializer<Calendar> {

    public static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
    public static final Locale LOCALE_HUNGARIAN = new Locale("hu", "HU");
    public static final TimeZone LOCAL_TIME_ZONE = TimeZone.getTimeZone("Europe/Budapest");

    @Override
    public void serialize(Calendar value, JsonGenerator gen, SerializerProvider arg2)
            throws IOException, JsonProcessingException {
        if (value == null) {
            gen.writeNull();
        } else {
            gen.writeString(FORMATTER.format(value.getTime()));
        }
    }
}

Deserializator klasa:

public class CustomCalendarDeserializer extends JsonDeserializer<Calendar> {

    @Override
    public Calendar deserialize(JsonParser jsonparser, DeserializationContext context)
            throws IOException, JsonProcessingException {
        String dateAsString = jsonparser.getText();
        try {
            Date date = CustomCalendarSerializer.FORMATTER.parse(dateAsString);
            Calendar calendar = Calendar.getInstance(
                CustomCalendarSerializer.LOCAL_TIME_ZONE, 
                CustomCalendarSerializer.LOCALE_HUNGARIAN
            );
            calendar.setTime(date);
            return calendar;
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}

i wykorzystanie powyższych klas:

public class CalendarEntry {

    @JsonSerialize(using = CustomCalendarSerializer.class)
    @JsonDeserialize(using = CustomCalendarDeserializer.class)
    private Calendar calendar;

    // ... additional things ...
}

Korzystając z tej implementacji, wykonanie procesu serializacji i deserializacji kolejno powoduje uzyskanie wartości pochodzenia.

Tylko przy użyciu adnotacji @JsonFormat deserializacja daje inny wynik. Myślę, że z powodu domyślnej konfiguracji wewnętrznej strefy czasowej biblioteki, czego nie można zmienić za pomocą parametrów adnotacji (takie było również moje doświadczenie z biblioteką Jacksona w wersji 2.5.3 i 2.6.3).

Miklos Krivan
źródło
4
Wczoraj otrzymałem negatywną opinię. Dużo pracowałem nad tym tematem, więc nie rozumiem. Czy mogę uzyskać informacje zwrotne, aby się z tego nauczyć? Byłbym wdzięczny za uwagi w przypadku negatywnego głosu. W ten sposób możemy dowiedzieć się od siebie więcej.
Miklos Krivan,
Świetna odpowiedź, dzięki, naprawdę mi pomogła! Drobna sugestia - rozważ umieszczenie CustomCalendarSerializer i CustomCalendarDeserializer jako klas statycznych w otaczającej klasie nadrzędnej. Myślę, że to uczyniłoby kod trochę ładniejszym :)
Stuart
@Stuart - powinieneś po prostu zaproponować tę reorganizację kodu jako kolejną odpowiedź lub zaproponować zmianę. Miklos może nie mieć na to czasu.
ocodo
@MiklosKrivan Głosowałem za tobą z kilku powodów. Powinieneś wiedzieć, że SimpleDateFormat nie jest bezpieczny dla wątków i szczerze mówiąc, powinieneś użyć alternatywnych bibliotek do formatowania daty (Joda, Commons-lang FastDateFormat itp.). Innym powodem jest ustawienie strefy czasowej, a nawet lokalizacji. O wiele lepiej jest używać GMT w środowisku serializacji i pozwolić klientowi powiedzieć, w której strefie czasowej się znajduje, lub nawet dołączyć preferowane tz jako oddzielny ciąg. Ustaw serwer na GMT, czyli UTC. Jackson ma wbudowany format ISO.
Adam Gent
1
Thx @AdamGent za twoją opinię. Rozumiem i akceptuję Twoje sugestie. Ale w tym konkretnym przypadku chciałem tylko skupić się na tym, że adnotacja JsonFormat z informacjami o lokalizacji nie działa tak, jak byśmy tego oczekiwali. I jak można to rozwiązać.
Miklos Krivan
4

Kompletny przykład aplikacji rozruchu sprężynowego w RFC3339formacie datetime

package bj.demo;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;

import java.text.SimpleDateFormat;

/**
 * Created by [email protected] at 2018/5/4 10:22
 */
@SpringBootApplication
public class BarApp implements ApplicationListener<ApplicationReadyEvent> {

    public static void main(String[] args) {
        SpringApplication.run(BarApp.class, args);
    }

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"));
    }
}
BaiJiFeiLong
źródło
4

Aby dodać znaki, takie jak T i Z, do daty

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
private Date currentTime;

wynik

{
    "currentTime": "2019-12-11T11:40:49Z"
}
Odwori
źródło
3

Pracuje dla mnie. SpringBoot.

 import com.alibaba.fastjson.annotation.JSONField;

 @JSONField(format = "yyyy-MM-dd HH:mm:ss")  
 private Date createTime;

wynik:

{ 
   "createTime": "2019-06-14 13:07:21"
}
anson
źródło
2

Opierając się na bardzo pomocnej odpowiedzi @ miklov-kriven, mam nadzieję, że te dwa dodatkowe punkty do rozważenia okażą się pomocne dla kogoś:

(1) Uważam, że dobrym pomysłem jest włączenie serializatora i deserializatora jako statycznych klas wewnętrznych w tej samej klasie. Uwaga: użycie ThreadLocal do zabezpieczenia wątków SimpleDateFormat.

public class DateConverter {

    private static final ThreadLocal<SimpleDateFormat> sdf = 
        ThreadLocal.<SimpleDateFormat>withInitial(
                () -> {return new SimpleDateFormat("yyyy-MM-dd HH:mm a z");});

    public static class Serialize extends JsonSerializer<Date> {
        @Override
        public void serialize(Date value, JsonGenerator jgen SerializerProvider provider) throws Exception {
            if (value == null) {
                jgen.writeNull();
            }
            else {
                jgen.writeString(sdf.get().format(value));
            }
        }
    }

    public static class Deserialize extends JsonDeserializer<Date> {
        @Overrride
        public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws Exception {
            String dateAsString = jp.getText();
            try {
                if (Strings.isNullOrEmpty(dateAsString)) {
                    return null;
                }
                else {
                    return new Date(sdf.get().parse(dateAsString).getTime());
                }
            }
            catch (ParseException pe) {
                throw new RuntimeException(pe);
            }
        }
    }
}

(2) Jako alternatywę dla używania adnotacji @JsonSerialize i @JsonDeserialize na każdym elemencie klasy możesz również rozważyć nadpisanie domyślnej serializacji Jacksona poprzez zastosowanie niestandardowej serializacji na poziomie aplikacji, czyli wszyscy członkowie klasy typu Date zostaną serializowani przez Jacksona przy użyciu tej niestandardowej serializacji bez jawnej adnotacji w każdym polu. Jeśli na przykład używasz Spring Boot, jeden ze sposobów na to wygląda następująco:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public Module customModule() {
        SimpleModule module = new SimpleModule();
        module.addSerializer(Date.class, new DateConverter.Serialize());
        module.addDeserializer(Date.class, new Dateconverter.Deserialize());
        return module;
    }
}
Stuart
źródło
Głosowałem na Ciebie (nienawidzę tego głosu, ale nie chcę, aby ludzie używali Twojej odpowiedzi). SimpleDateFormat nie obsługuje wątków. Jest rok 2016 (odpowiedziałeś w 2016). Nie należy używać SimpleDateFormat, gdy istnieje wiele szybszych i bezpiecznych wątków opcji. Jest nawet dokładne niewłaściwe wykorzystanie tego, co proponujesz Q / A tutaj: stackoverflow.com/questions/25680728/…
Adam Gent
2
@AdamGent dzięki za wskazanie tego. W tym kontekście, używając Jacksona, klasa ObjectMapper jest bezpieczna wątkowo, więc nie ma to znaczenia. Jednak rozumiem, że kod może być kopiowany i używany w kontekście bezpiecznym bez wątków. Dlatego zmodyfikowałem moją odpowiedź, aby zapewnić bezpieczny dostęp do wątku SimpleDateFormat. Przyjmuję również do wiadomości, że istnieją alternatywy, głównie pakiet java.time.
Stuart
2

Jeśli ktoś ma problemy z użyciem niestandardowego formatu daty dla java.sql.Date, to jest to najprostsze rozwiązanie:

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(java.sql.Date.class, new DateSerializer());
mapper.registerModule(module);

(Ta odpowiedź SO zaoszczędziła mi wielu problemów: https://stackoverflow.com/a/35212795/3149048 )

Jackson domyślnie używa SqlDateSerializer dla java.sql.Date, ale obecnie ten serializator nie bierze pod uwagę formatu daty, zobacz ten problem: https://github.com/FasterXML/jackson-databind/issues/1407 . Aby obejść ten problem, należy zarejestrować inny serializator dla java.sql.Date, jak pokazano w przykładzie kodu.

Stephanie
źródło
1

Chcę zaznaczyć, że ustawienie SimpleDateFormatpolubienia opisanego w drugiej odpowiedzi działa tylko w przypadku, java.util.Dategdy zakładam, że chodzi o to pytanie. Ale java.sql.Dateformater nie działa. W moim przypadku nie było zbyt oczywiste, dlaczego formater nie działał, ponieważ w modelu, który powinien być serializowany, pole było w rzeczywistości a, java.utl.Dateale rzeczywisty obiekt ostatecznie był a java.sql.Date. Jest to możliwe, ponieważ

public class java.sql extends java.util.Date

Więc to jest rzeczywiście ważne

java.util.Date date = new java.sql.Date(1542381115815L);

Więc jeśli zastanawiasz się, dlaczego twoje pole Data nie jest poprawnie sformatowane, upewnij się, że obiekt to naprawdę java.util.Date.

Tutaj również wspomniano, dlaczego obsługa java.sql.Datenie zostanie dodana.

To byłaby przełomowa zmiana i nie sądzę, żeby to było uzasadnione. Gdybyśmy zaczynali od zera, zgodziłbym się ze zmianą, ale nie jest tak dużo.

mosiężna małpa
źródło
1
Dziękuję za wskazanie jednej konsekwencji tego złego projektu dwóch różnych Dateklas. Jednak obecnie nie powinno się żadnego z nich ani też SimpleDateFormat. java.time, nowoczesny interfejs API daty i czasu w Javie, zastąpił je prawie 5 lat temu. Użyj go i FasterXML / jackson-modules-java8 .
Ole VV