Java y json con jackson

De ChuWiki
Saltar a: navegación, buscar

Jackson es una librería java que permite convertir clases a texto JSON y viceversa. De forma similar Gson realiza el mismo trabajo, sin embargo, cuando hay clases hijas, padres, interfaces y polimorfismo en general, Gson tiene algo de soporte extra (debe descargarse por separado), y no es tan sencillo como en Jackson (necesita registrar previamente la jerarquía de clases).

Veamos aquí un ejemplo de polimorfismo con Jackson. Tienes el código completo del ejemplo en [ https://github.com/chuidiang/chuidiang-ejemplos/tree/master/JAVA_SE/src/com/chuidiang/ejemplos/jackson Github]

Dependencia Maven

Para tener jackson en nuestro proyecto maven, la dependencia que hay que poner es esta

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>2.8.3</version>
	<scope>compile</scope>
</dependency>

Convertir a JSON

Para convertir nuestras clases a JSON, necesitamos crear un ObjectMapper de Jackson y luego simplemente usarlo. Tiene muchos métodos para ello, pero los fáciles serían estos

// Creamos el ObjectMapper por defecto
ObjectMapper theBadMapper = new ObjectMapper();

// Creamos la clase que queramos convertir a JSON, por ejemplo, un ArrayList de AnInterface
ArrayList<AnInterface> childs = .... 

// Convertir a JSON
String theJsonText = theBadMapper.writeValueAsString(childs);

// Volver a obtener la clase original
ArrayList<AnInterface> reconstructedChilds = theBadMapper.readValue(
    theJsonText, 
    new TypeReference<ArrayList<AnInterface>>() {} );

No hay mucho truco. Se crea un ObjectMapper con la configuración por defecto y se usa su método writeValueAsString() para obtener el texto JSON correspondiente a esa clase.

Para recuperar el objeto, podemos usar el método readValue() al que pasamos el texto JSON obtenido anteriormente y una instancia de TypeReference indicando qué tipo queremos que nos devuelva, un ArrayList<AnInterface> en nuestro ejemplo.

Con clases normales (sin herencias), todo esto funciona estupendamente.

Problema con herencias

Hay sin embargo un problema cuando hay herencias, sobre todo interfaces y clases abstractas. En nuestro ejemplo, AnInterface es una interface y de ella hay una clase que la implementa que hemos llamado AParentClass y de ella hemos hecho un par de hijos AChildClass y AnotherChildClass. Y hemos sido malos y rellenamos el ArrayList así :

ArrayList<AnInterface> childs = new ArrayList<>(2);
childs.add(new AChildClass());
childs.add(new AnotherChildClass());

es decir, decimos que en el ArrayList se van a guardar interfaces, y metemos en él dos clases concretas que implementan esa interface. Cuando ejecutamos nuestro programa anterior con el ObjectMapper por defecto e intentamos recuperar las clases a partir del JSON, obtenemos una excepción que dice

Can not construct instance of com.chuidiang.ejemplos.jackson.AnInterface

es decir, como a ObjectMapper le decimos que el ArrayList es de una interface AnInterface, intenta instanciarla para rellenar el código. Y las interfaces no pueden instanciarse, así que error al canto.

ObjectMapper con polimorfismo

La forma fácil de solucionar esto con Jackson, sin necesidad de poner anotaciones ni registrar el árbol de herencias, consiste en decirle a Jackson que cuando obtenga el JSON ponga una marca con cada clase concreta que ha convertido en JSON. El siguiente código hace esto

ArrayList<AnInterface> childs = new ArrayList<>(2);
childs.add(new AChildClass());
childs.add(new AnotherChildClass());
...
ObjectMapper theGoodMapper = new ObjectMapper();
theGoodMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

String theJsonText = theGoodMapper.writeValueAsString(childs);
System.out.println(theJsonText);

y el JSON que obtenemos tiene esta pinta

["java.util.ArrayList", [
	["com.chuidiang.ejemplos.jackson.AChildClass", {
		"anAttribute": 0,
		"anotherAttribute": null,
		"aChildAttribute": null
	}],
	["com.chuidiang.ejemplos.jackson.AnotherChildClass", {
		"anAttribute": 0,
		"anotherAttribute": null,
		"anotherChildAttribute": 0.0
	}]
]]

Vemos que ha puesto que por cada clase java que convierte a JSON, lo hace como un array de dos elementos JSON. El primero indica el nombre de la clase concreta y el segundo su contenido. Así, tenemos el ArrayList

["java.util.ArrayList", [ el contenido del arrayList ]]

y cada elemento del ArrayList es a su vez un array con dos elementos

["com.chuidiang.ejemplos.jackson.AChildClass", { el contenido de AChildClass }]

De esta forma, el ObjectMapper luego es capaz de volver a obtener las clases originales de forma fácil

ArrayList<AnInterface> reconstructedChilds = theGoodMapper.readValue(
    theJsonText, new TypeReference<ArrayList<AnInterface>>() {});
System.out.println("The good mapper works ! ");
System.out.println(reconstructedChilds);

Esto funciona bien, sin excepciones, y la última salida por pantalla nos muestra que ha construido las clases correctas

The good mapper works ! 
[com.chuidiang.ejemplos.jackson.AChildClass@5622fdf, com.chuidiang.ejemplos.jackson.AnotherChildClass@4883b407]