2.2.2. Jackson#
Otra librería muy utilizada y que tiene la ventaja de que también soporta XML es la proporcionada por el proyecto Jackson.
Como GSON, nos permite hacer una conversión automática entre el modelo de objetos y JSON para lo cual podemos definir las clases para Tutor, Alumno y Grupo exactamente de la misma forma. Como en aquel caso, empezaremos por no complicar las cosas: las secuencias son arrays y los valores son directamente traducibles a un tipo primitivo de JSON (es decir, representaremos la edad y no la fecha de nacimiento).
Para empezar necesitaremos la librería jackson-databind.
2.2.2.1. Lectura#
El código es muy similar al practicado con GSON:
Path ruta = Path.of(System.getProperty("user.home"), "grupos.json");
ObjectMapper mapper = new ObjectMapper();
try (
InputStream st = Files.newInputStream(ruta);
InputStreamReader sr = new InputStreamReader(st);
) {
Grupo[] grupos = mapper.readValue(sr, Grupo[].class);
Arrays.stream(grupos).forEach(System.out::println);
}
catch(IOException err) {
err.printStackTrace();
}
Si, en cambio, quisiéramos generar una lista de grupos y no un array:
TypeReference GrupoLista = new TypeReference<ArrayList<Grupo>>(){};
ArrayList<Grupo> grupos = mapper.readValue(sr, GrupoLista);
Nota
Que los miembros de cada grupo sean un array o una lista es absolutamente indiferente: la traducción se hará de igual forma.
2.2.2.2. Escritura#
Por su parte la escritura a un JSON desde un modelo de objetos tampoco tiene excesivas diferencias respecto a lo que encontraríamos en GSON:
public static void main(String[] args) {
Path ruta = Path.of(System.getProperty("java.io.tmpdir"), "grupos.json");
ObjectMapper mapper = new ObjectMapper();
// mapper.enable(SerializationFeature.INDENT_OUTPUT); // Salida "bonita"
Grupo[] grupos = {
new Grupo(
(short) 1,
"ESO",
'B',
new Tutor("Pepe M.J.", "Matemáticas"),
new Alumno[] {new Alumno("Pablito", 12), new Alumno("Juanito", 13)}
),
new Grupo(
(short) 2,
"ESO",
'C',
new Tutor("Pedro J.M.", "Lengua"),
new Alumno[] {new Alumno("Lola", 13), new Alumno("Manolito", 13)}
)
};
try (
OutputStream st = Files.newOutputStream(ruta);
OutputStreamWriter sw = new OutputStreamWriter(st);
) {
mapper.writeValue(sw, grupos);
}
catch (IOException err) {
err.printStackTrace();
}
}
Al escribir, es indiferente si usamos arrays o listas.
Nota
Existe el método writeValueAsString
que devuelve una cadena con el
JSON resultante:
String contenido = mapper.writeValueAsString(grupos);
2.2.2.3. Tipos no primitivos#
En el ejemplo anterior todos los tipos de los objetos de Java tienen
correspondencia con un tipo primitivo de JSON, ya que las cadenas son cadenas,
los números, números, y los arrays o ArrayList
los traduce directamente
Jackson como secuencias. Incluso si usamos los valores de una enumeración para
codificarla en el JSON[1], la librería será capaz de traducir sin
instrucciones por nuestra parte.
2.2.2.4. Fechas#
Pero supongamos ahora que no es así y que de los alumnos se registra la fecha de nacimiento:
[
{
"nivel": 1,
"etapa": "ESO",
"grupo": "D",
"tutor": {
"nombre": "Federico Arias Torres",
"especialidad": "Inglés"
},
"miembros": [
{
"nombre": "Matías Sánchez Aguado",
"nacimiento": "2012-01-02"
},
{
"nombre": "María Bonet Periáñez",
"nacimiento": "2012-02-21"
}
]
},
{
"nivel": 2,
"etapa": "ESO",
"grupo": "A",
"tutor": {
"nombre": "Gertrudis Avelladena Pérez",
"especialidad": "Francés"
},
"miembros": [
{
"nombre": "Marcela Venegas Pancorbo",
"nacimiento": "2011-08-09"
},
{
"nombre": "Feliciano Martín Suárez",
"nacimiento": "2011-09-15"
}
]
}
]
y, por supuesto, se modifica la clase Alumno
para que contenga un atributo
nacimiento de tipo Date (véase la versión incluida
en las explicaciones sobre GSON).
Date
La clase Date del paquete java.util tiene soporte nativo en la librería, pero su traducción es un número entero que representa el tiempo UNIX). Si esta representación no nos gusta y preferimos otra (p.e. “yyyy-MM-dd”), entonces tendremos que indicarlo y podemos usar dos estrategias:
Especificar el formato de fechas en el objeto mapeador:
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
Usar la anotación @JsonFormat que da soporte a fechas:
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd") private Date nacimiento
Nota
En este segundo caso, necesitamos importar el paquete aparte jackson-annotations.
Advertencia
Al usar esta anotación puede haber desajustes al serializar las fechas como consecuencia de la zona horaria, por lo que habría que especificarla en la propia anotación:
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd", timezone="Europe/Madrid") private Date nacimiento
o al definir el objeto de mapeo:
mapper.setTimeZone(TimeZone.getDefault());
Prudencia
En las anotaciones deben usarse literales y constantes, por lo que no puede usarse el método para obtener el huso horario del sistema.
Por hacer
Comprobar si es preciso incorporar:
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
LocalDate
Las clases más modernas incluidas en el paquete java.time no tienen soporte nativo en a librería y necesitan que se importe el módulo jackson-datatype-jsr310. Hecho esto, la representación de estas clases se hará según la ISO 8601 (que para una fecha LocalDate es “yyyy-MM-dd”). Si se quieren otras representaciones, entonces habrá que informar a la librería y las estrategias cambia un poco respecto a Date:
La anotación se hace exactamente igual que con Date:
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd") private LocalDate nacimiento
En cambio, no hay un soporte directo equivalente al que proporcionaba el módulo
setDateFormat
y es un poco más complicado hacer algo semejante:DateTimeFormatter pattern = DateTimeFormatter.ofPattern("dd/MM/yyyy"); SimpleModule module = new SimpleModule(); module.addSerializer(LocalDate.class, new LocalDateSerializer(pattern)) module.addDeserializer(LocalDate.class, new LocalDateDeserializer(pattern)); // Se supone ya creado el mapper. mapper.registerModule(module);
Por hacer
Comprobar que no aporta nada a este respecto usar JavaTimeModule en vez de SimpleModule.
2.2.2.5. Traductor específico#
La librería incluye un mecanismo para hacer traducciones totalmente arbitrarias de cualquier tipo que se nos pueda ocurrir, Basta con añadir, en principio, la librería jackson-core) para poder hacer cualquier definición. Ilustrémoslo con la serialización y deserialización artesanal de una tipo Date, aunque ya esté resuelto dentro de la propia librería. Sin embargo, nos sirve para ilustrarlo[2]:
public class Traductor {
public static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
public static class DateSerializer extends JsonSerializer<Date> {
@Override
public void serialize(Date value, JsonGenerator gen, SerializerProvider sp)
throws IOException, JsonProcessingException {
if(value == null) gen.writeNull();
else gen.writeString(df.format(value)); // Date --> String
}
}
public static class DateDeserializer extends JsonDeserializer<Date> {
@Override
public Date deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
try {
return df.parse(parser.getText()); // String --> Date
}
catch(ParseException err) {
throw new RuntimeException(err);
}
}
}
}
En definitiva, al serializar tenemos que indicar cómo pasar del tipo a una cadena (que será la que se escriba en el JSON), y al deserializar cómo pasar de la cadena que aparece en el JSON al tipo de Java correspondiente.
Y ya sólo quedaría indicar el campo en que se va a usar este traductor:
@JsonSerialize(using=Traductor.DateSerializer.class)
@JsonDeserialize(using=Traductor.DateDeserializer.class)
private Date nacimiento;
Y en caso de que hubiera varios atributos Date en las clases implicadas y todas se quieran traducir igual, podríamos ahorrarnos la anotación individual en cada uno de los atributos y hacer lo siguiente:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(java.util.Date.class, new Traductor.DateSerializer());
module.addDeserializer(java.util.Date.class, new Traductor.DateDeserializer());
mapper.registerModule(module);
Truco
Obsérvese que el segundo mecanismo para convertir fechas LocalDate es equivalente a este que indicamos ahora. Simplemente, la librería adicional jackson-datatype-jsr310 ya define clases serializadoras y deserializadoras para los tipos incluidos en java.time y nos limitamos a usarlas en vez de definirlas nosotros.
Notas al pie