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 鈥嬧媏n 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 *