¿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:
Te puede interesar:Manejo de Excepciones en Java: Una guía completa con las mejores y peores prácticas{
"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:
Te puede interesar:Cómo desarrollar un proyecto Maven en EclipseException 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?
Te puede interesar:Cómo configurar los ajustes de red en JavaBueno 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.
Te puede interesar:Cómo usar hilos en Java SwingJackson
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_REFERENCES | Una 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_OUTPUT | Una 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_KEYES | Caracterí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_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 (). |
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_TIMESTAMPS | Una 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_NANOSECONDS | Una 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_TIMESTAMPS | Una 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_ID | Una 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:
Te puede interesar:Ejemplo: carga de una clase Java en tiempo de ejecuciónException 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!
Te puede interesar:Pruebas unitarias en Java con JUnit 5Person{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.