Asignación de formato de fecha a JSON Jackson

154

Tengo un formato de fecha proveniente de API como este:

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

Que es AAAA-DD-MM HH: MM am / pm GMT marca de tiempo. Estoy asignando este valor a una variable de fecha en POJO. Obviamente, muestra un error de conversión.

Me gustaría saber 2 cosas:

  1. ¿Cuál es el formato que necesito usar para llevar a cabo la conversión con Jackson? ¿Date es un buen tipo de campo para esto?
  2. En general, ¿hay alguna forma de procesar las variables antes de que Jackson las asigne a los miembros de Object? Algo así como, cambiar el formato, cálculos, etc.
Kevin Rave
fuente
Este es realmente un buen ejemplo, coloque la anotación en el campo de la clase: java.dzone.com/articles/how-serialize-javautildate
digz6666
Ahora tienen una página wiki para el manejo de la fecha: wiki.fasterxml.com/JacksonFAQDateHandling
Sutra
En estos días ya no deberías usar la Dateclase. java.time, la moderna API de fecha y hora de Java, la reemplazó hace casi 5 años. Úselo y FasterXML / jackson-modules-java8 .
Ole VV

Respuestas:

125

¿Cuál es el formato que necesito usar para llevar a cabo la conversión con Jackson? ¿Date es un buen tipo de campo para esto?

Datees un buen tipo de campo para esto. Puede hacer que el análisis JSON sea bastante fácil usando ObjectMapper.setDateFormat:

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

En general, ¿hay alguna forma de procesar las variables antes de que Jackson las asigne a los miembros de Object? Algo así como, cambiar el formato, cálculos, etc.

Si. Tiene algunas opciones, incluida la implementación de una costumbre JsonDeserializer, por ejemplo, extender JsonDeserializer<Date>. Este es un buen comienzo.

pb2q
fuente
2
El formato de 12 horas es mejor si el formato también incluye la designación AM / PM: DateFormat df = new SimpleDateFormat ("aaaa-MM-dd hh: mm a z");
John Scattergood
Intenté todas estas soluciones, pero no pude almacenar una variable de fecha de mi POJO en un valor clave de mapa, también como fecha. Quiero que esto cree una instancia de un BasicDbObject (MongoDB API) del Mapa y, en consecuencia, almacene la variable en una colección de MongoDB DB como Fecha (no como Long o String). ¿Es esto posible? Gracias
RedEagle
1
¿Es tan fácil usar Java 8 LocalDateTimeo en ZonedDateTimelugar de Date? Dado que Dateestá básicamente en desuso (o al menos muchos de sus métodos), me gustaría usar esas alternativas.
houcros
Los javadocs para setSateFormat () dicen que esta llamada hace que ObjectMapper ya no sea seguro para subprocesos. Creé las clases JsonSerializer y JsonDeserializer.
MiguelMunoz
Como la pregunta no menciona java.util.Dateexplícitamente, quiero señalar que esto no funciona para java.sql.Date.Ver también mi respuesta a continuación.
mono de latón
329

Desde Jackson v2.0, puede usar la anotación @JsonFormat directamente en los miembros de Object;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm a z")
private Date date;
Olivier Lecrivain
fuente
61
Si desea incluir la zona horaria:@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
realPK
Hola, qué tarro tiene esa anotación. Estoy usando la versión jackson-mapper-asl 1.9.10. No lo entiendo
krishna Ram
1
@Ramki: anotaciones de jackson> = 2.0
Olivier Lecrivain
3
Esta anotación solo funciona perfectamente en la fase de serialización, pero durante la deserialización, la información de zona horaria y configuración regional no se utiliza en absoluto. He intentado timezone = "CET" y timezone "Europe / Budapest" con locale = "hu", pero ninguno de los dos funciona y causa una conversación extraña en el calendario. Solo la serialización personalizada con deserialización funcionó para mí manejando la zona horaria según sea necesario. Aquí está el tutorial perfecto sobre cómo debe usar baeldung.com/jackson-serialize-dates
Miklos Krivan
1
Este es un proyecto jar separado llamado Jackson Annotations . Entrada de muestra pom
bub
52

Por supuesto, hay una forma automatizada llamada serialización y deserialización y puede definirla con anotaciones específicas ( @JsonSerialize , @JsonDeserialize ) como también se menciona en pb2q.

Puede usar java.util.Date y java.util.Calendar ... y probablemente también JodaTime.

Las anotaciones @JsonFormat no funcionaron para mí como quería (ha ajustado la zona horaria a un valor diferente) durante la deserialización (la serialización funcionó a la perfección):

@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")

Debe usar el serializador personalizado y el deserializador personalizado en lugar de la anotación @JsonFormat si desea un resultado previsto. He encontrado un muy buen tutorial y solución aquí http://www.baeldung.com/jackson-serialize-dates

Hay ejemplos para los campos de Fecha , pero los necesitaba para los campos de Calendario , así que aquí está mi implementación :

La clase de serializador :

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()));
        }
    }
}

La clase de deserializador :

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);
        }
    }
}

y el uso de las clases anteriores:

public class CalendarEntry {

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

    // ... additional things ...
}

Con esta implementación, la ejecución del proceso de serialización y deserialización da como resultado consecutivamente el valor de origen.

Solo usando la anotación @JsonFormat, la deserialización da un resultado diferente , creo que debido a la configuración predeterminada de la zona horaria interna de la biblioteca, lo que no puede cambiar con los parámetros de anotación (esa fue mi experiencia con la biblioteca Jackson 2.5.3 y la versión 2.6.3 también).

Miklos Krivan
fuente
44
Ayer recibí un voto negativo por mi respuesta. Trabajé mucho en este tema, así que no entiendo. ¿Puedo tener algún comentario para aprender de él? Agradecería algunas notas en caso de voto negativo. De esta manera podemos aprender más unos de otros.
Miklos Krivan
Gran respuesta, gracias, ¡realmente me ayudó! Sugerencia menor: considere colocar CustomCalendarSerializer y CustomCalendarDeserializer como clases estáticas dentro de una clase principal adjunta. Creo que esto haría que el código sea un poco más agradable :)
Stuart
@Stuart: simplemente debe ofrecer esta reorganización del código como otra respuesta, o proponer una edición. Miklos puede no tener el tiempo libre para hacerlo.
ocodo
@MiklosKrivan Te he rechazado por varias razones. Debe saber que SimpleDateFormat no es seguro para subprocesos y, francamente, debe usar bibliotecas alternativas para el formato de fecha (Joda, Commons-lang FastDateFormat, etc.). La otra razón es la configuración de la zona horaria e incluso la configuración regional. Es mucho más preferible usar GMT en un entorno de serialización y dejar que su cliente indique en qué zona horaria se encuentra o incluso adjunte el tz preferido como una cadena separada. Configure su servidor para estar en GMT, también conocido como UTC. Jackson tiene el formato ISO incorporado.
Adam Gent
1
Thx @AdamGent por sus comentarios. Entiendo y acepto sus sugerencias. Pero en este caso particular, solo quería centrarme en que la anotación JsonFormat con información local no funciona de la manera que esperaríamos. Y cómo se puede resolver.
Miklos Krivan
4

Solo un ejemplo completo para la aplicación de arranque de primavera con RFC3339formato de fecha y hora

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
fuente
4

Para agregar caracteres como T y Z en su fecha

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

salida

{
    "currentTime": "2019-12-11T11:40:49Z"
}
Odwori
fuente
3

Trabajando para mi SpringBoot.

 import com.alibaba.fastjson.annotation.JSONField;

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

salida:

{ 
   "createTime": "2019-06-14 13:07:21"
}
anson
fuente
2

Sobre la base de la respuesta muy útil de @ miklov-kriven, espero que estos dos puntos adicionales de consideración sean útiles para alguien:

(1) Me parece una buena idea incluir el serializador y el deserializador como clases internas estáticas en la misma clase. NB, utilizando ThreadLocal para la seguridad de subprocesos de 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) Como alternativa al uso de las anotaciones @JsonSerialize y @JsonDeserialize en cada miembro individual de la clase, también podría considerar anular la serialización predeterminada de Jackson aplicando la serialización personalizada a nivel de aplicación, es decir, todos los miembros de la clase de tipo Fecha serán serializados por Jackson utilizando esta serialización personalizada sin anotaciones explícitas en cada campo. Si está utilizando Spring Boot, por ejemplo, una forma de hacerlo sería la siguiente:

@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
fuente
Te rechacé (odio el voto negativo pero no quiero que la gente use tu respuesta). SimpleDateFormat no es seguro para subprocesos. Es 2016 (respondiste en 2016). No debe usar SimpleDateFormat cuando hay numerosas opciones más rápidas y seguras. Incluso hay un mal uso exacto de lo que está proponiendo P / R aquí: stackoverflow.com/questions/25680728/…
Adam Gent
2
@AdamGent gracias por señalar esto. En este contexto, con Jackson, la clase ObjectMapper es segura para subprocesos, por lo que no importa. Sin embargo, considero que el código puede copiarse y usarse en un contexto no seguro para subprocesos. Así que he editado mi respuesta para hacer que el acceso al hilo SimpleDateFormat sea seguro. También reconozco que hay alternativas, principalmente el paquete java.time.
Stuart
2

Si alguien tiene problemas con el uso de un formato de fecha personalizado para java.sql.Date, esta es la solución más simple:

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

(Esta respuesta SO me ahorró muchos problemas: https://stackoverflow.com/a/35212795/3149048 )

Jackson usa el SqlDateSerializer de forma predeterminada para java.sql.Date, pero actualmente, este serializador no tiene en cuenta el formato de fecha, vea este problema: https://github.com/FasterXML/jackson-databind/issues/1407 . La solución consiste en registrar un serializador diferente para java.sql.Date como se muestra en el ejemplo de código.

Stephanie
fuente
1

Quiero señalar que establecer un elemento SimpleDateFormatsimilar al descrito en la otra respuesta solo funciona para java.util.Datelo que supongo que se entiende en la pregunta. Pero para java.sql.Dateel formateador no funciona. En mi caso, no era muy obvio por qué el formateador no funcionaba porque en el modelo que debería ser serializado el campo era de hecho un java.utl.Datepero el objeto real terminó siendo un java.sql.Date. Esto es posible porque

public class java.sql extends java.util.Date

Entonces esto es realmente válido

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

Entonces, si se pregunta por qué su campo Fecha no está formateado correctamente, asegúrese de que el objeto sea realmente un java.util.Date.

Aquí también se menciona por qué java.sql.Dateno se agregará el manejo .

Esto sería un cambio radical, y no creo que esté justificado. Si comenzáramos desde cero, estaría de acuerdo con el cambio, pero como las cosas no son tanto.

mono de latón
fuente
1
Gracias por señalar una consecuencia de este mal diseño de las dos Dateclases diferentes . Sin embargo, en estos días uno no debería tener ninguno de ellos SimpleDateFormat. java.time, la API moderna de fecha y hora de Java, los reemplazó hace casi 5 años. Úselo y FasterXML / jackson-modules-java8 .
Ole VV