Leer y escribir JSON en Java

    ¿Qué es JSON?

    Notación de objetos 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 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 varios 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 muchas bases de datos, las últimas versiones de PostgreSQL y MySQL contienen el soporte nativo para consultar los datos almacenados en campos JSON. Bases de datos NoSQL como MongoDB se construyeron sobre este formato y utilizan documentos JSON para almacenar registros, al igual que las tablas y 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 esquema, no es necesario llevar una sobrecarga estructural masiva como espacios de nombres y envoltorios.

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

    • Instrumentos de String
    • Números
    • Booleanos
    • Matrices
    • Objetos
    • nulo

    Echemos un vistazo a un documento JSON simple:

    {
      "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. Cadena que representa un objeto, por lo que 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, deberíamos agregar una nueva dependencia que nos lo proporcione. Para empezar, usaremos el json-simple módulo, agregándolo como una dependencia de Maven.

    <dependency>
        <groupId>com.googlecode.json-simple</groupId>
        <artifactId>json-simple</artifactId>
        <version>{version}</version>
    </dependency>
    

    Este módulo es totalmente compatible con la especificación JSON RFC4627 y proporciona una funcionalidad básica como codificar y decodificar objetos JSON y no tiene ninguna dependencia de módulos externos.

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

    public static void writeJsonSimpleDemo(String filename) throws Exception {
        JSONObject sampleObject = new JSONObject();
        sampleObject.put("name", "Pharos.shr");
        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 del JSONObject clase, poniendo un nombre y una edad como propiedades. Entonces estamos creando una instancia de la clase. JSONArray sumando dos elementos de cadena y poniéndolo como una tercera propiedad de nuestro sampleObject. Al final, nos estamos transformando sampleObject a un documento JSON que llama al toJSONString() método y escribirlo en un archivo.

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

    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 hemos puesto:

    {"name":"Pharos.shr","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 en él 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 completamente o imprimir las propiedades individuales seleccionadas:

    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 señalar que el parse() el método devuelve un Object 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:

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

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

    Cavar más profundo

    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:

    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 lo ponemos en el example.json archivo:

    {
      "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 del Person clase. Intentemos hacer esto usando simple-json primero.

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

    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 muchos tipos raros encasillados, pero parece funcionar, ¿verdad?

    Bueno en realidad no…

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

    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:

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

    pero el segundo lanza un Exception:

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

    El problema aquí es que nuestro encasillado a un List<Person> no creo dos nuevos Person objetos, simplemente metió en lo que había allí – un JSONObject en nuestro caso actual. Cuando intentamos profundizar y obtener la edad real del primer niño, nos encontramos con un 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 una manera muy eficiente se llama Jackson. Es muy común y se usa en proyectos de grandes empresas como Hibernar.

    Agreguémoslo como una nueva dependencia de Maven:

    <dependency> 
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>{version}</version>
    </dependency>
    

    La clase principal 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 una serie de opciones diferentes pasadas al constructor:

    FAIL_ON_SELF_REFERENCESUna característica que determina lo que sucede cuando un POJO detecta una autorreferencia directa (y no se habilita el manejo de Id. De objeto): o se lanza una JsonMappingException (si es verdadera) o la referencia se procesa normalmente (falsa).
    INDENT_OUTPUTUna función que permite habilitar (o deshabilitar) la sangría para el generador subyacente, utilizando la bonita impresora predeterminada configurada para ObjectMapper (y ObjectWriters creados a partir del asignador).
    ORDER_MAP_ENTRIES_BY_KEYESCaracterística que determina si las entradas del mapa se ordenan primero por clave antes de la serialización o no: si está habilitado, se realiza un paso de clasificación adicional si es necesario (no es necesario para SortedMaps), si está deshabilitado, no se necesita una clasificación adicional.
    USE_EQUALITY_FOR_OBJECT_IDCaracterí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 ().
    Una característica que determina cómo el tipo char[] está serializado: cuando está habilitado, se serializará como una matriz JSON explícita (con cadenas de un solo carácter como valores); cuando está deshabilitado, por defecto los serializa como Strings (que es más compacto).
    WRITE_DATE_KEYS_AS_TIMESTAMPSUna función que determina si las fechas (y 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_NANOSECONDSUna función que controla si los valores numéricos de la marca de tiempo deben escribirse 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_TIMESTAMPSUna función que determina si los valores de fecha (y fecha / hora) (y elementos basados ​​en fecha como calendarios) se deben serializar como marcas de tiempo numéricas (verdadero, predeterminado) o como otra cosa (generalmente representación textual).
    WRITE_DATES_WITH_ZONE_IDUna característica que determina si los valores de fecha / fecha-hora deben ser serializados para que incluyan la identificación de la zona horaria, en los casos en que el tipo en sí contiene información de la zona horaria.

    Una lista completa de SerializationFeature enum está disponible aquí.

    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 código, obtendremos una excepción:

    Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class com.Pharos.sh.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 al Person clase:

    public Person() {}
    

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

    Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "isMarried" (class com.Pharos.sh.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 nombres de propiedad de captadores, eliminando las primeras partes de ellos. En el caso de getAge() y getName() funciona perfectamente, pero con isMarried() no lo hace y asume que el campo debe llamarse married en vez de isMarried.

    Una opción brutal, pero que funciona: podemos resolver este problema simplemente cambiando el nombre del getter a isIsMarried. Sigamos adelante e intentemos hacer esto.

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

    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 is a cada uno de sus getters booleanos.

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

    @JsonProperty(value="isMarried")
    public boolean isMarried() {
        return isMarried;
    }
    

    De esta manera le decimos explícitamente a Jackson el nombre del campo y no tiene que adivinar. Podría ser especialmente útil en los casos en que el nombre del campo sea totalmente diferente al de los getters.

    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 JSON en Java, sin embargo, hay varios módulos que brindan esta funcionalidad. En este tutorial, hemos cubierto los json-simple y Jackson módulos, 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.

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *