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