2.2.1. GSON#

La librería GSON es desarrollada por Google y nos permite dos estrategias para leer y escribir JSON:

  • Procesar el archivo y realizar la conversión manualmente a clases de Java.

  • Realizar la conversión a clases automáticamente.

Nota

Podemos descargar la librería de su página en mvnrepository.com.

2.2.1.1. Conversión automática#

Para ello necesitamos que las clases con las que vamos a convertir los objetos codificados en JSON sigan el estándar JavaBean.

Es obvio que en el ejemplo anterior tenemos tres objetos (Grupo, Alumno y Tutor) así que definámoslos, de modo que sus atributos se ajusten a lo que recoge el JSON:

Tutor.java#
import java.io.Serializable;

public class Tutor implements Serializable {
    private String nombre;
    private String especialidad;

    public Tutor() {}

    public Tutor(String nombre, String especialidad) {
        setNombre(nombre);
        setEspecialidad(especialidad);
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public String getEspecialidad() {
        return especialidad;
    }

    public void setEspecialidad(String especialidad) {
        this.especialidad = especialidad;
    }

    @Override
    public String toString() {
        return nombre;
    }
}
Alumno.java#
import java.io.Serializable;

public class Alumno implements Serializable {
    private String nombre;
    private int edad;

    public Alumno() {}

    public Alumno(String nombre, int edad) {
        setNombre(nombre);
        setEdad(edad);
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public int getEdad() {
        return edad;
    }

    public void setEdad(int edad) {
        this.edad = edad;
    }

    @Override
    public String toString() {
        return String.format("%s: %d", nombre, edad);
    }
}
Grupo.java#
import java.io.Serializable;
import java.util.Arrays;

public class Grupo implements Serializable {
    private short nivel;
    private String etapa;
    private char grupo;
    private Tutor tutor;
    private Alumno[] miembros;

    public Grupo() {}

    public Grupo(short nivel, String etapa, char grupo, Tutor tutor, Alumno[] miembros) {
        setNivel(nivel);
        setEtapa(etapa);
        setGrupo(grupo);
        setTutor(tutor);
        setMiembros(miembros);
    }

    public short getNivel() {
        return nivel;
    }

    public void setNivel(short nivel) {
        this.nivel = nivel;
    }

    public String getEtapa() {
        return etapa;
    }

    public void setEtapa(String etapa) {
        this.etapa =  etapa;
    }

    public char getGrupo() {
        return grupo;
    }

    public void setGrupo(char grupo) {
        this.grupo = grupo;
    }

    public Tutor getTutor() {
        return tutor;
    }

    public void setTutor(Tutor tutor) {
        this.tutor = tutor;
    }

    public Alumno[] getMiembros() {
        return miembros;
    }

    public void setMiembros(Alumno[] miembros) {
        this.miembros = miembros;
    }

    public String nombre() {
        return String.format("%dº%s-%c", nivel, etapa, grupo);
    }

    @Override
    public String toString() {
        return String.format("%s (%s): %s", nombre(), tutor, Arrays.toString(miembros));
    }
}

No nos hemos roto mucho la cabeza: estas tres clases recogen las propiedades que se observan en el JSON y cumplen los requisitos para ser un JavaBean:

  • Disponen de un constructor sin argumentos.

  • Es serializable.

  • Sus atributos son privados.

  • Dispone de getters y setters con la convención de nombres apropiada.

2.2.1.1.1. Lectura#

La lectura es sumamente sencilla y se basa en crear un objeto GSON:

Path ruta = Path.of(System.getProperty("user.home"), "grupos.json");
Gson gson = new Gson();

try (
   InputStream st = Files.newInputStream(ruta);
   InputStreamReader sr = new InputStreamReader(st);
) {
   Grupo[] grupos = gson.fromJson(sr, Grupo[].class);
   for(Grupo g: grupos) {
      System.out.println(g);
   }
}
catch(IOException err) {
   err.printStackTrace();
}

Obsérvese que, dado que el archivo es una secuencia de objetos Grupo al usar el método fromJson, indicamos que la traducción se haga como Grupo[].class. Si quisiéramos que grupos fuera una lista y no un array, entonces tendríamos que complicar un poco el código:

Type GrupoLista = new TypeToken<ArrayList<Grupo>>() {}.getType();
ArrayList<Grupo> grupos = gson.fromJson(sr, GrupoLista);

Nota

En cambio, no tenemos que preocuparnos por el atributo Grupo.miembros: si en vez de haberlo definido como un array lo hubiéramos definido como un ArrayList, todo nuestro código sería válido.

2.2.1.1.2. Escritura#

La escritura también es bastante sencilla:

Path ruta = Path.of(System.getProperty("user.home"), "grupos.json");
Gson gson = new Gson();

Grupo[] grupo = {
    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);
) {
    gson.toJson(grupo, sw);
}
catch(IOException err) {
    err.printStackTrace();
}

En este caso, se escribe el archivo grupos.json con todos los campos apelotonados. Si queremos alterar este modo predeterminado podemos construir un objeto Gson utilizando GsonBuilder:

Gson gson = new GsonBuilder()
      .setPrettyPrinting()
      .setDateFormat(DateFormat.SHORT, DateFormat.DEFAULT) // Definimos cómo queremos almacenar fecha y hora.
      .create()

Nota

Hemos aprovechado para indicar cómo traducir los objetos de tiempo a una cadena en el archivo JSON. Para conocer más sobre estos formatos eche un vistazo al tutorial sobre DateFormat. También podríamos optar por definir más explícitamente el formato según lo expuesto para SimpleDateFormat:

Gson gson = new GsonBuilder()
      .setPrettyPrinting()
      .setDateFormat("yyyy-MM-dd") // Por ejemplo, 2024-09-03
      .create()

También puede generarse una cadena en vez de escribir directamente en un archivo:

String contenido = gson.toJson(grupo);

Y usar posteriormente esta cadena para lo que nos interese (p.e. guardarla en un archivo).

2.2.1.1.3. Tipos no primitivos#

En el ejemplo anterior, los valores de todos los campos eran tipos primitivos de JSON y, en consecuencia, su traducción al modelo de objetos de Java no revestía problemas. Supongamos, sin embargo, el mismo ejemplo, pero con una pequeña diferencia: de los alumnos no se expresa la edad, sino su 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"
         }
      ]
   }
]

Como el estándar JSON no tiene definido un campo específico para las fechas, las hemos expresado, simplemente, con una cadena con un formato apropiado y de este modo aparecen en el archivo.

Nota

La librería ya proporciona una solución predefinida para el caso particular del tipo Date y que se citó un poco más arriba:

Gson gson = new GsonBuilder()
      .setPrettyPrinting()
      .setDateFormat("yyyy-MM-dd") // Por ejemplo, 2024-09-03
      .create()

Con crear así el objeto GSON y que el atributo Alumno.nacimiento sea de tipo Date ya se estará definiendo cómo es la traducción entre el objeto y el campo JSON (una cadena de la forma indicada). Pero esta traducción sólo es posible para fechas, y no para otro tipo de objetos para los cuales necesitaremos definir nosotros la traducción. Como queremos ilustrar cómo se hace esta traducción, prescindimos de la ayuda de setDateFormat.

Para lograr la traducción entre una cadena con formato yyyy-MM-dd y el tipo Date necesitamos definir así al alumno:

Alumno.java#
import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import com.google.gson.annotations.JsonAdapter;

public class Alumno implements Serializable {
    private String nombre;
    
    @JsonAdapter(YyyyMMddDateTypeAdapter.class)
    private Date nacimiento;

    private static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");

    public Alumno() {}

    public Alumno(String nombre, String nacimiento) throws ParseException {
        setNombre(nombre);
        setNacimiento(nacimiento);
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public Date getNacimiento() {
        return nacimiento;
    }

    public void setNacimiento(Date nacimiento) {
        this.nacimiento = nacimiento;
    }

    public void setNacimiento(String nacimiento) throws ParseException {
        this.nacimiento = df.parse(nacimiento);
    }

    @Override
    public String toString() {
        return String.format("%s: %s", nombre, df.format(nacimiento));
    }
}

¿Qué hemos hecho? Básicamente:

  • La trivialidad de cambiar todo lo pertinente para que el campo entero edad pase a ser nacimiento de tipo Date

  • Hemos, no obstante, añadido un segundo setter a nacimiento para permitir que se pueda definir usando una cadena en vez de un objeto Date. Esto, en realidad, no tiene nada que ver con la traducción y se hace para poder definir manualmente en la aplicación alumnos de esta forma:

    Alumno lola = new Alumno("Lola", "2011-08-11");
    
  • Declaramos que para el campo nacimiento usaremos un adaptador:

    @JsonAdapter(YyyyMMddDateTypeAdapter.class)
    private Date nacimiento;
    

Y ahora hay que definir esa clase que define cómo es la traducción. Lo más limpio, por si se quiere soportar fácilmente diferentes formatos para la fecha es crear una clase abstracta:

AbstractDateTypeAdapter.java#
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

abstract class AbstractDateTypeAdapter extends TypeAdapter<Date> {

    protected abstract DateFormat getDateFormat();

    @Override
    @SuppressWarnings("resource")
    public final void write(final JsonWriter out, final Date value) throws IOException {
        out.value(getDateFormat().format(value));  // Traduce Date a una cadena "yyyy-MM-dd"
    }

    @Override
    public final Date read(final JsonReader in) {
        try {
            return getDateFormat().parse(in.nextString());
        }
        catch (Exception err) {
            return null;
        }
    }
}

Y una clase que concretamente define cuál es el formato de fecha:

YyyyyMMddDateTypeAdapter.java#
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class YyyyMMddDateTypeAdapter extends AbstractDateTypeAdapter {
    private YyyyMMddDateTypeAdapter() {}

    @Override
    protected DateFormat getDateFormat() {
        return new SimpleDateFormat("yyyy-MM-dd");
    }
}

La parte molar de la traducción está en la clase abstracta:

  • El método out es el que se encarga de tomar el atributo del objeto y convertirlo en el valor JSON: por eso uno de sus argumentos un tipo Date.

  • El método in es el que hace la traducción inversa: de la cadena JSON se obtiene el atributo de tipo Date

2.2.1.2. Conversión manual#

Por hacer

Tómese como referencia inicial GSON en Java con ejemplos..