Leer y escribir JSON en Java

La notación de objetos de JavaScript o, en resumen, JSON es un formato de intercambio de datos que se introdujo en 1999 y se adoptó ampliamente a mediados de la década de 2000. Actualmente, es...

¿Qué es JSON?

La notación de objetos de JavaScript o, en resumen, [JSON] (https://www.json.org/) es un formato de intercambio de datos que se introdujo en 1999 y se adoptó ampliamente a mediados de la década de 2000. Actualmente, es el formato estándar de facto para la comunicación entre los servicios web y sus clientes (navegadores, aplicaciones móviles, etc.). Saber leerlo y escribirlo es una habilidad esencial para cualquier desarrollador de software.

Aunque JSON se derivó de JavaScript, es un formato independiente de la plataforma. Puede trabajar con él en múltiples lenguajes de programación, incluidos Java, Python, Ruby y muchos más. Realmente, cualquier lenguaje que pueda analizar una cadena puede manejar JSON.

La popularidad de JSON resultó en su soporte nativo por parte de muchas bases de datos, las últimas versiones de postgresql y mysql contienen el soporte nativo para consultando los datos almacenados en campos JSON. Las bases de datos NoSQL como MongoDB se crearon sobre este formato y usan documentos JSON para almacenar registros, al igual que las tablas y las filas almacenan registros en una base de datos relacional.

Una de las principales ventajas de JSON, en comparación con el formato de datos XML, es el tamaño del documento. Como JSON no tiene esquemas, no hay necesidad de llevar una sobrecarga estructural masiva como espacios de nombres y contenedores.

JSON es un formato de datos genérico que tiene seis tipos de datos:

  • Cuerdas
  • Números
  • Booleanos
  • Matrices
  • Objetos
  • nulo

Echemos un vistazo a un documento JSON simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "name": "Benjamin Watson",
  "age": 31,
  "isMarried": true,
  "hobbies": ["Football", "Swimming"],
  "kids": [
    {
      "name": "Billy",
      "age": 5
    }, 
   {
      "name": "Milly",
      "age": 3
    }
  ]
}

Esta estructura define un objeto que representa a una persona llamada "Benjamin Watson". Podemos ver sus detalles aquí, como su edad, estado familiar y pasatiempos.

En esencia, el objeto JSON no es más que una cadena. Una cadena que representa un objeto, razón por la cual los objetos JSON a menudo se denominan Cadenas JSON o Documentos JSON.

json-simple

Como no hay soporte nativo para JSON en Java, en primer lugar, debemos agregar una nueva dependencia que nos lo proporcione. Para empezar, usaremos el módulo json-simple, agregándolo como una dependencia de Maven.

1
2
3
4
5
<dependency>
    <groupId>com.googlecode.json-simple</groupId>
    <artifactId>json-simple</artifactId>
    <version>{version}</version>
</dependency>

Este módulo cumple totalmente con la especificación JSON RFC4627 y proporciona funciones básicas, como la codificación y decodificación de objetos JSON, y no tiene dependencias externas. módulos.

Vamos a crear un método simple que tomará un nombre de archivo como parámetro y escribirá algunos datos JSON codificados:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static void writeJsonSimpleDemo(String filename) throws Exception {
    JSONObject sampleObject = new JSONObject();
    sampleObject.put("name", "wikihtpr");
    sampleObject.put("age", 35);

    JSONArray messages = new JSONArray();
    messages.add("Hey!");
    messages.add("What's up?!");

    sampleObject.put("messages", messages);
    Files.write(Paths.get(filename), sampleObject.toJSONString().getBytes());
}

Aquí, estamos creando una instancia de la clase JSONObject, poniendo un nombre y una edad como propiedades. Luego estamos creando una instancia de la clase JSONArray agregando dos elementos de cadena y colocándolos como una tercera propiedad de nuestro sampleObject. En última instancia, estamos transformando sampleObject en un documento JSON llamando al método toJSONString() y escribiéndolo en un archivo.

Para ejecutar este código, debemos crear un punto de entrada a nuestra aplicación que podría verse así:

1
2
3
4
5
public class Solution {
    public static void main(String[] args) throws Exception {
        writeJsonSimpleDemo("example.json");
    }
}

Como resultado de ejecutar este código, obtendremos un archivo llamado example.json en la raíz de nuestro paquete. El contenido del archivo será un documento JSON, con todas las propiedades que le hemos puesto:

1
{"name":"wikihtpr","messages":["Hey!","What's up?!"],"age":35}

¡Excelente! Acabamos de tener nuestra primera experiencia con el formato JSON y hemos serializado con éxito un objeto Java y lo hemos escrito en el archivo.

Ahora, con una ligera modificación de nuestro código fuente, podemos leer el objeto JSON del archivo e imprimirlo en la consola por completo o imprimir las propiedades individuales seleccionadas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static void main(String[] args) throws Exception {
    JSONObject jsonObject = (JSONObject) readJsonSimpleDemo("example.json");
    System.out.println(jsonObject);
    System.out.println(jsonObject.get("age"));
}
    
public static Object readJsonSimpleDemo(String filename) throws Exception {
    FileReader reader = new FileReader(filename);
    JSONParser jsonParser = new JSONParser();
    return jsonParser.parse(reader);
}

Es importante tener en cuenta que el método parse() devuelve un Objeto y tenemos que convertirlo explícitamente en JSONObject.

Si tiene un documento JSON mal formado o dañado, obtendrá una excepción similar a esta:

1
Exception in thread "main" Unexpected token END OF FILE at position 64.

Para simularlo, intente eliminar el último corchete de cierre }.

Profundizando

Aunque json-simple es útil, no nos permite usar clases personalizadas sin escribir código adicional. Supongamos que tenemos una clase que representa a una persona de nuestro ejemplo inicial:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person {
    Person(String name, int age, boolean isMarried, List<String> hobbies,
            List<Person> kids) {
        this.name = name;
        this.age = age;
        this.isMarried = isMarried;
        this.hobbies = hobbies;
        this.kids = kids;
    }

    Person(String name, int age) {
        this(name, age, false, null, null);
    }

    private String name;
    private Integer age;
    private Boolean isMarried;
    private List<String> hobbies;
    private List<Person> kids;

    // getters and setters

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", isMarried=" + isMarried +
                ", hobbies=" + hobbies +
                ", kids=" + kids +
                '}';
    }
}

Tomemos el documento JSON que usamos como ejemplo al principio y colóquelo en el archivo example.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "name": "Benjamin Watson",
  "age": 31,
  "isMarried": true,
  "hobbies": ["Football", "Swimming"],
  "kids": [
    {
      "name": "Billy",
      "age": 5
    }, 
   {
      "name": "Milly",
      "age": 3
    }
  ]
}

Nuestra tarea sería deserializar este objeto de un archivo a una instancia de la clase Person. Intentemos hacer esto usando simple-json primero.

Modificando nuestro método main(), reutilizando el readSimpleJsonDemo() estático y agregando las importaciones necesarias, llegaremos a:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static void main(String[] args) throws Exception {
    JSONObject jsonObject = (JSONObject) readJsonSimpleDemo("example.json");
    Person ben = new Person(
                (String) jsonObject.get("name"),
                Integer.valueOf(jsonObject.get("age").toString()),
                (Boolean) jsonObject.get("isMarried"),
                (List<String>) jsonObject.get("hobbies"),
                (List<Person>) jsonObject.get("kids"));

    System.out.println(ben);
}

No se ve muy bien, tenemos un montón de encasillamientos extraños, pero parece hacer el trabajo, ¿verdad?

Bueno en realidad no...

Intentemos imprimir en la consola la matriz kids de nuestra Person y luego la edad del primer niño.

1
2
System.out.println(ben.getKids());
System.out.println(ben.getKids().get(0).getAge());

Como vemos, la primera salida de la consola muestra un resultado aparentemente bueno de:

1
[{"name":"Billy","age":5},{"name":"Milly","age":3}]

pero el segundo arroja una Excepción:

1
Exception in thread "main" java.lang.ClassCastException: org.json.simple.JSONObject cannot be cast to com.wikihtp.json.Person

El problema aquí es que nuestra conversión de tipos a List<Person> no creó dos nuevos objetos Person, simplemente rellenó lo que estaba allí - un JSONObject en nuestro caso actual. Cuando tratamos de profundizar y obtener la edad real del primer niño, nos encontramos con una ClassCastException.

Este es un gran problema que estoy seguro de que podrá superar escribiendo un montón de código muy inteligente del que podría estar orgulloso, pero hay una manera sencilla de hacerlo bien desde el principio.

###Jackson

Una biblioteca que nos permitirá hacer todo esto de manera muy eficiente se llama jackson. Es muy común y se usa en proyectos de grandes empresas como Hibernar.

Vamos a agregarlo como una nueva dependencia de Maven:

1
2
3
4
5
<dependency> 
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>{version}</version>
</dependency>

La clase central que usaremos se llama ObjectMapper, tiene un método readValue() que toma dos argumentos: una fuente para leer y una clase para enviar el resultado.

ObjectMapper podría configurarse con varias opciones diferentes pasadas al constructor:


FAIL_ON_SELF_REFERENCES  Una función que determina qué sucede cuando un POJO detecta una autorreferencia directa (y no se habilita el manejo de ID de objeto): se lanza una excepción JsonMappingException (si es verdadero) o la referencia se procesa normalmente (falso). INDENT_OUTPUT Una característica que permite habilitar (o deshabilitar) la sangría para el generador subyacente, utilizando la impresora bonita predeterminada configurada para ObjectMapper (y ObjectWriters creados a partir de mapper). ORDER_MAP_ENTRIES_BY_KEYES Característica que determina si las entradas del mapa se ordenan primero por clave antes de la serialización o no: si está habilitada, se realiza un paso de clasificación adicional si es necesario (no es necesario para SortedMaps), si está desactivada, no se necesita una clasificación adicional. USE_EQUALITY_FOR_OBJECT_ID Característica que determina si la Identidad del objeto se compara utilizando la verdadera identidad del objeto a nivel de JVM (falso); o, método equals(). A feature that determines how type char[] is serialized: when enabled, will be serialized as an explicit JSON array (with single-character Strings as values); when disabled, defaults to serializing them as Strings (which is more compact). WRITE_DATE_KEYS_AS_TIMESTAMPS Una función que determina si las fechas (y los subtipos) utilizados como claves de mapa se serializan como marcas de tiempo o no (si no, se serializarán como valores textuales). WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS Una característica que controla si los valores de marca de tiempo numéricos se escribirán usando marcas de tiempo de nanosegundos (habilitado) o no (deshabilitado); si y solo si el tipo de datos admite dicha resolución. WRITE_DATES_AS_TIMESTAMPS Una función que determina si los valores de fecha (y fecha/hora) (y cosas basadas en fechas como calendarios) deben serializarse como marcas de tiempo numéricas (verdadero; el valor predeterminado) o como otra cosa (generalmente representación textual). WRITE_DATES_WITH_ZONE_ID Una característica que determina si los valores de fecha/fecha-hora deben serializarse para que incluyan la identificación de la zona horaria, en los casos en que el tipo contiene información de la zona horaria.


Una lista completa de la enumeración SerializationFeature está disponible aquí.

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    Person ben = objectMapper.readValue(new File("example.json"), Person.class);
    System.out.println(ben);
    System.out.println(ben.getKids());
    System.out.println(ben.getKids().get(0).getAge());
}

Desafortunadamente, después de ejecutar este fragmento de código, obtendremos una excepción:

1
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class com.wikihtp.json.Person]: can not instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)

Por lo que parece, tenemos que agregar el constructor predeterminado a la clase Person:

1
public Person() {}

Al volver a ejecutar el código, veremos aparecer otra excepción:

1
Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "isMarried" (class com.wikihtp.json.Person), not marked as ignorable (5 known properties: "hobbies", "name", "married", "kids", "age"])

Este es un poco más difícil de resolver ya que el mensaje de error no nos dice qué hacer para lograr el resultado deseado. Ignorar la propiedad no es una opción viable, ya que claramente la tenemos en el documento JSON y queremos que se traduzca al objeto Java resultante.

El problema aquí está relacionado con la estructura interna de la biblioteca Jackson. Deriva los nombres de propiedad de captadores, eliminando las primeras partes de ellos. En el caso de getAge() y getName() funciona perfectamente, pero con isMarried() no funciona y asume que el campo debe llamarse married en lugar de isMarried.

Una opción brutal, pero funcional: podemos resolver este problema simplemente cambiando el nombre del getter a isIsMarried. Sigamos adelante y tratemos de hacer esto.

¡No aparecen más excepciones y vemos el resultado deseado!

1
2
3
4
5
Person{name='Benjamin Watson', age=31, isMarried=true, hobbies=[Football, Swimming], kids=[Person{name='Billy', age=5, isMarried=null, hobbies=null, kids=null}, Person{name='Milly', age=3, isMarried=null, hobbies=null, kids=null}]}

[Person{name='Billy', age=5, isMarried=null, hobbies=null, kids=null}, Person{name='Milly', age=3, isMarried=null, hobbies=null, kids=null}]

5

Aunque el resultado es satisfactorio, hay una mejor manera de evitar esto que agregar otro “es” a cada uno de sus captadores booleanos.

Podemos lograr el mismo resultado agregando una anotación al método isMarried():

1
2
3
4
@JsonProperty(value="isMarried")
public boolean isMarried() {
    return isMarried;
}

De esta manera, le estamos diciendo explícitamente a Jackson el nombre del campo y no tiene que adivinarlo. Podría ser especialmente útil en los casos en que el nombre del campo sea totalmente diferente al de los captadores.

Conclusión

JSON es un formato ligero basado en texto que nos permite representar objetos y transferirlos a través de la web o almacenarlos en la base de datos.

No hay soporte nativo para la manipulación de JSON en Java, sin embargo, hay varios módulos que brindan esta funcionalidad. En este tutorial, hemos cubierto los módulos json-simple y Jackson, mostrando las fortalezas y debilidades de cada uno de ellos.

Al trabajar con JSON, debe tener en cuenta los matices de los módulos con los que está trabajando y depurar las excepciones que podrían aparecer con cuidado.

Licensed under CC BY-NC-SA 4.0