Serializar y deserializar XML en Java con Jackson

En un ecosistema cada vez más conectado de sistemas de software, la comunicación entre ellos se ha vuelto aún más primordial. A su vez, varias tecnologías han sido...

Introducción

En un ecosistema cada vez más conectado de sistemas de software, la comunicación entre ellos se ha vuelto aún más primordial. A su vez, se han desarrollado varias tecnologías para empaquetar los datos que se transfieren o comparten entre estos muchos y diferentes sistemas.

El eXtensible Markup Language, conocido popularmente como XML, es una de las formas de empaquetar los datos que se van a transferir. XML es un lenguaje de formato de documentos que se desarrolló en la década de 1990 ya que HTML no permite la definición de nuevos elementos de texto, es decir, no es extensible. Además de ser extensible, los datos en XML se describen a sí mismos, lo que los hace legibles y fáciles de comprender.

En esta publicación, exploraremos la manipulación de XML en Java usando la biblioteca jackson.

Ventajas y desventajas de XML

XML sigue siendo popular y se usa en algunos sistemas, ya que tiene algunas ventajas, pero también han surgido tecnologías más nuevas para solucionar algunas de sus deficiencias.

Algunas de las ventajas de XML incluyen:

  • XML ​​no está vinculado a una sola plataforma o lenguaje de programación y se puede usar fácilmente en muchos sistemas diferentes. Esto lo hace adecuado para facilitar la comunicación entre sistemas con diferentes configuraciones de hardware y software.
  • Los datos contenidos en un documento XML se pueden validar utilizando una definición de tipo de documento (DTD) o un esquema XML. Este es un conjunto de declaraciones de marcado que definen los componentes básicos de un documento XML.
  • A través de su soporte para Unicode, XML puede contener información escrita en cualquier idioma o formato sin perder ninguna información o contenido en el proceso.
  • A través de su compatibilidad con HTML, es fácil leer y mostrar datos contenidos en un documento XML usando HTML.
  • La información almacenada en un documento XML puede modificarse en cualquier momento sin afectar la presentación de los datos a través de otros medios como HTML.

Algunas de las deficiencias de XML que se han resuelto en las nuevas tecnologías incluyen:

  • La sintaxis es bastante redundante y detallada en comparación con otros formatos, como JSON, que es breve y directo.
  • Debido a su sintaxis y naturaleza detallada, los documentos XML suelen ser grandes, lo que puede generar costos adicionales de almacenamiento y transporte.
  • No tiene soporte para arreglos.

Bibliotecas XML

La manipulación de XML en Java puede ser un proceso tedioso, por lo que para facilitar el proceso y acelerar el desarrollo, existen varias bibliotecas que podemos usar. Incluyen:

  • fácil que es una biblioteca pequeña y sencilla para construir, manipular, analizar y buscar XML.
  • Arquitectura Java para enlace XML (JAXB) es un marco para mapear clases Java a representaciones XML a través de ordenar objetos Java en XML y desarmar XML en objetos Java . Es parte de la plataforma Java SE.
  • jackson es una biblioteca para manejar JSON en sistemas Java y ahora tiene soporte para XML desde la versión 2.
  • DOM4J es una biblioteca de uso eficiente de la memoria para analizar XML, XPath y XSLT (lenguaje extensible de hojas de estilo).
  • JDom que es una biblioteca de análisis XML compatible con XPath y XSLT.

¿Qué es Jackson?

El proyecto Jackson es una colección de herramientas de procesamiento de datos para el lenguaje Java y la plataforma JVM. Admite una amplia gama de formatos de datos como CSV, propiedades de Java, XML y YAML a través de componentes de extensión que admiten el lenguaje específico.

El componente Jackson XML está diseñado para leer y escribir datos XML emulando cómo funciona JAXB, aunque no de manera concluyente.

En este artículo, usaremos la biblioteca Jackson para serializar objetos Java en XML y deserializarlos nuevamente en objetos Java.

Configuración del proyecto

Primero, configuremos un nuevo proyecto Maven:

1
$ mvn archetype:generate -DgroupId=com.wikihtp -DartifactId=xmltutorial -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

Con nuestro proyecto generado, agreguemos la dependencia de Jackson en nuestro archivo pom.xml. Elimine la sección de dependencias existente y reemplácela con:

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
<dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>3.8.1</version>
    <scope>test</scope>
  </dependency>

  <!-- Jackson dependency for XML manipulation -->
  <dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.0</version>
  </dependency>
</dependencies>

<build>
  <plugins>
    <!--
    This plugin configuration will enable Maven to include the project dependencies
    in the produced jar file.
    It also enables us to run the jar file using `java -jar command`
    -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <version>3.2.0</version>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>shade</goal>
          </goals>
          <configuration>
            <transformers>
              <transformer
                  implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>com.wikihtp.App</mainClass>
              </transformer>
            </transformers>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Ahora podemos probar el proyecto que hemos configurado ejecutando los siguientes comandos:

1
2
$ mvn package
$ java -jar target/java -jar target/xmltutorial-1.0.jar

El resultado debe ser Hello World! impreso en nuestra terminal, lo que muestra que nuestro proyecto está listo para el siguiente paso del proyecto.

Serialización de objetos Java en XML

Los objetos de Java tienen atributos y métodos para manipular estos atributos. En relación con un documento XML, los elementos del documento se pueden asignar a los atributos de un objeto Java.

En el proceso de serialización, los atributos de un objeto se convierten en elementos XML y se almacenan en un documento XML.

Usaremos una clase PhoneDetails que definirá información sobre un modelo de teléfono en particular, como su nombre, tamaño de pantalla y capacidad de almacenamiento interno. En nuestra clase, estos serán atributos, pero en nuestro documento XML, estos detalles estarán contenidos en etiquetas o elementos.

Comencemos definiendo la clase PhoneDetails que se usará para generar nuestros objetos:

1
2
3
4
5
6
7
public class PhoneDetails {
    private String name;
    private String displaySize;
    private String memory;

    // getters and setters
}

Con nuestro conjunto de objetos, modifiquemos nuestro App.java y agreguemos una función para manejar la serialización a XML:

 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
/**
* This function writes serializes the Java object into XML and writes it
* into an XML file.
*/
public static void serializeToXML() {
    try {
        XmlMapper xmlMapper = new XmlMapper();

        // serialize our Object into XML string
        String xmlString = xmlMapper.writeValueAsString(new PhoneDetails("OnePlus", "6.4", "6/64 GB"));

        // write to the console
        System.out.println(xmlString);

        // write XML string to file
        File xmlOutput = new File("serialized.xml");
        FileWriter fileWriter = new FileWriter(xmlOutput);
        fileWriter.write(xmlString);
        fileWriter.close();
    } catch (JsonProcessingException e) {
        // handle exception
    } catch (IOException e) {
        // handle exception
    }
}

public static void main(String[] args) {
    System.out.println("Serializing to XML...");
    serializeToXML();
}

Empaquetamos y ejecutamos nuestro proyecto una vez más:

1
2
$ mvn package
$ java -jar target/xmltutorial-1.0.jar

La salida en la terminal es:

1
<PhoneDetails><name>OnePlus</name><displaySize>6.4</displaySize><memory>6/64 GB</memory></PhoneDetails>

En la carpeta raíz de nuestro proyecto, se crea el archivo serialized.xml que contiene esta información. Hemos serializado con éxito nuestro objeto Java en XML y lo hemos escrito en un archivo XML.

En nuestra función serializeToXML(), creamos un objeto XmlMapper, que es una clase secundaria de la clase ObjectMapper utilizada en la serialización JSON. Esta clase convierte nuestro objeto Java en una salida XML que ahora podemos escribir en un archivo.

Deserialización desde XML

Jackson también nos permite leer el contenido de un archivo XML y deserializar la cadena XML nuevamente en un objeto Java. En nuestro ejemplo, leeremos un documento XML que contiene detalles sobre un teléfono y usaremos Jackson para extraer estos datos y usarlos para crear objetos Java que contengan la misma información.

Primero, creemos un documento XML que coincida con nuestra clase para leer. Cree to_deserialize.xml con los siguientes contenidos:

1
2
3
4
5
<PhoneDetails>
  <name>iPhone</name>
  <displaySize>6.2</displaySize>
  <memory>3/64 GB</memory>
</PhoneDetails>

Agreguemos una función deserializeFromXML() para deserializar el archivo XML anterior en un objeto Java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void deserializeFromXML() {
    try {
        XmlMapper xmlMapper = new XmlMapper();

        // read file and put contents into the string
        String readContent = new String(Files.readAllBytes(Paths.get("to_deserialize.xml")));

        // deserialize from the XML into a Phone object
        PhoneDetails deserializedData = xmlMapper.readValue(readContent, PhoneDetails.class);

        // Print object details
        System.out.println("Deserialized data: ");
        System.out.println("\tName: " + deserializedData.getName());
        System.out.println("\tMemory: " + deserializedData.getMemory());
        System.out.println("\tDisplay Size: " + deserializedData.getDisplaySize());
    } catch (IOException e) {
        // handle the exception
    }
}

public static void main(String[] args) {
    System.out.println("Deserializing from XML...");
    deserializeFromXML();
}

Empaquetamos y ejecutamos nuestro proyecto como de costumbre y el resultado es:

1
2
3
4
5
6
Deserializing from XML...

Deserialized data:
    Name: iPhone
    Memory: 3/64 GB
    Display Size: 6.2

Nuestro archivo XML se ha deserializado con éxito y todos los datos se han extraído con la ayuda de la biblioteca Jackson.

Anotaciones de Jackson {#anotaciones de Jackson}

Las anotaciones se utilizan para agregar metadatos a nuestro código Java y no tienen un efecto directo en la ejecución del código al que están adjuntas. Se utilizan para dar instrucciones al compilador durante el tiempo de compilación y el tiempo de ejecución.

Jackson usa anotaciones para varias funciones, como definir si estamos mapeando a XML o JSON, definir el orden de los atributos y campos en nuestra salida o sus nombres.

Estas anotaciones generalmente se aplican en nuestros Java POJO (Plain Old Java Objects). Por ejemplo, podemos anotar nuestra clase PhoneDetails de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class PhoneDetails {

    @JsonProperty("phone_name")
    private String name;

    @JsonProperty("display_size")
    private String displaySize;

    @JsonProperty("internal_memory")
    private String memory;

    // rest of the code remains as is
}

La anotación @JsonProperty ayuda a definir el nombre de los campos en nuestro archivo XML. Con esta anotación agregada, las etiquetas en nuestros archivos de entrada y salida XML tendrán que parecerse a las cadenas en la anotación de la siguiente manera:

1
2
3
4
5
<PhoneDetails>
  <phone_name>OnePlus</phone_name>
  <display_size>6.4</display_size>
  <internal_memory>6/64 GB</internal_memory>
</PhoneDetails>

Otra anotación notable es @JacksonXmlText que indica que un elemento debe mostrarse como texto sin formato sin ninguna etiqueta u otro elemento que lo contenga.

La anotación @JacksonXmlProperty se puede usar para controlar los detalles del atributo o elemento que se muestra. Dichos detalles pueden incluir el espacio de nombres del elemento. Los espacios de nombres son una forma de asignar elementos a un grupo en particular.

Uno de los principales usos de los espacios de nombres es evitar conflictos cuando se utilizan etiquetas similares en el documento. Ayudan a aislar las etiquetas por grupo para eliminar cualquier ambigüedad que pueda surgir a medida que se escalan los documentos XML.

El orden de las propiedades también se puede especificar mediante una anotación @JsonPropertyOrder. Por ejemplo, para invertir el orden de los elementos en la salida del documento XML, la anotación se usa de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@JsonPropertyOrder({ "internal_memory", "display_size", "phone_name" })
public class PhoneDetails {

    @JsonProperty("phone_name")
    private String name;

    @JsonProperty("display_size")
    private String displaySize;

    @JsonProperty("internal_memory")
    private String memory;

    ...

La salida de la serialización a XML ahora será:

1
2
3
4
5
<PhoneDetails>
  <internal_memory>6/64 GB</internal_memory>
  <display_size>6.4</display_size>
  <phone_name>OnePlus</phone_name>
</PhoneDetails>

Si hay campos en los objetos de Java que no deseamos serializar, podemos usar la anotación @JsonIgnore y los campos se omitirán durante la serialización y deserialización.

Las anotaciones de Jackson son útiles para definir y controlar el proceso de serialización y deserialización en varios formatos, como XML, JSON y YAML. Algunas anotaciones funcionan para todos los formatos y otras están vinculadas a un tipo específico de archivo.

Se pueden encontrar más anotaciones de Jackson y sus usos en este [wiki oficial] (https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations) en Github.

Manipulación de elementos anidados y listas en XML

Habiendo aprendido acerca de las anotaciones, mejoremos nuestro archivo XML para agregar elementos y bucles anidados y modifiquemos nuestro código para serializar y deserializar la siguiente estructura actualizada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<PhoneDetails>
  <internal_memory>3/64 GB</internal_memory>
  <display_size>6.2</display_size>
  <phone_name>iPhone X</phone_name>
  <manufacturer>
    <manufacturer_name>Apple</manufacturer_name>
    <country>USA</country>
    <other_phones>
      <phone>iPhone 8</phone>
      <phone>iPhone 7</phone>
      <phone>iPhone 6</phone>
    </other_phones>
  </manufacturer>
</PhoneDetails>

En esta nueva estructura, hemos introducido un elemento ‘Fabricante’ anidado que también incluye una lista de elementos. Con nuestro código actual, no podemos extraer ni crear la nueva sección anidada.

Para arreglar esto, se requiere una nueva clase para manejar el elemento anidado, y en ese sentido, esto es parte de nuestra nueva clase Fabricante:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// define the order of elements
@JsonPropertyOrder({ "manufacturer_name", "country", "other_phones" })
public class Manufacturer {
    @JsonProperty("manufacturer_name")
    private String name;

    @JsonProperty("country")
    private String country;

    // new annotation
    @JacksonXmlElementWrapper(localName="other_phones")
    private List<String> phone;

    ...

Es bastante similar a nuestra clase PhoneDetails pero ahora hemos introducido una nueva anotación: @JacksonXmlElementWrapper. El propósito de esta anotación es definir si una colección de elementos usa o no un elemento envolvente, y se puede usar para dictar el nombre local y el espacio de nombres de los elementos envolventes.

En nuestro ejemplo, usamos la anotación para definir el elemento que contiene una lista de elementos y la etiqueta que se usará para ese elemento. Esto se usará al serializar y deserializar nuestros archivos XML.

Este cambio en nuestra estructura XML y la introducción de esta clase requiere que modifiquemos nuestra clase PhoneDetails para reflejar:

1
2
3
4
5
6
7
8
9
// existing code remains
public class PhoneDetails {
    // existing code remains
    @JsonProperty("manufacturer")
    private Manufacturer manufacturer;

    // standard getters and setters for the new element

    ...

Nuestro objeto PhoneDetails ahora podrá incluir información sobre el fabricante de un teléfono.

A continuación, actualizamos nuestro método serializeToXML():

 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
public static void serializeToXML() {
    try {
        XmlMapper xmlMapper = new XmlMapper();

        // create a list of other phones
        List<String> otherPhones = Arrays.asList("OnePlus 6T", "OnePlus 5T", "OnePlus 5");

        // create the manufacturer object
        Manufacturer manufacturer = new Manufacturer("OnePlus", "China", otherPhones);

        // serialize our new Object into XML string
        String xmlString = xmlMapper
          .writeValueAsString(new PhoneDetails("OnePlus", "6.4", "6/64 GB", manufacturer));

        // write to the console
        System.out.println(xmlString);

        // write XML string to file
        File xmlOutput = new File("serialized.xml");
        FileWriter fileWriter = new FileWriter(xmlOutput);
        fileWriter.write(xmlString);
        fileWriter.close();
    } catch (JsonProcessingException e) {
        // handle the exception
    } catch (IOException e) {
        // handle the exception
    }
}

El resultado de serializar el nuevo objeto ‘PhoneDetails’ con la información del ‘Fabricante’ es:

1
2
3
Serializing to XML...

<PhoneDetails><internal_memory>6/64 GB</internal_memory><display_size>6.4</display_size><phone_name>OnePlus</phone_name><manufacturer><manufacturer_name>OnePlus</manufacturer_name><country>China</country><other_phones><phones>OnePlus 6T</phones><phones>OnePlus 5T</phones><phones>OnePlus 5</phones></other_phones></manufacturer></PhoneDetails>

¡Funciona! Nuestra función deserializeFromXML(), por otro lado, no necesita una actualización importante ya que la clase PhoneDetails, cuando se deserializa, también incluirá información del fabricante.

Agreguemos el siguiente código para imprimir los detalles del fabricante solo para estar seguros:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// existing code remains

// Print object details
System.out.println("Deserialized data: ");
System.out.println("\tName: " + deserializedData.getName());
System.out.println("\tMemory: " + deserializedData.getMemory());
System.out.println("\tDisplay Size: " + deserializedData.getDisplaySize());
System.out.println("\tManufacturer Name: " + deserializedData.getManufacturer().getName());
System.out.println("\tManufacturer Country: " + deserializedData.getManufacturer().getCountry());
System.out.println("\tManufacturer Other Phones: " + deserializedData.getManufacturer().getPhone().toString());

// existing code remains

La salida:

1
2
3
4
5
6
7
8
9
Deserializing from XML...

Deserialized data:
    Name: iPhone X
    Memory: 3/64 GB
    Display Size: 6.2
    Manufacturer Name: Apple
    Manufacturer Country: USA
    Manufacturer Other Phones: [iPhone 8, iPhone 7, iPhone 6]

El proceso de deserialización es fluido y los nuevos detalles del fabricante se extrajeron de nuestro archivo XML actualizado.

Conclusión

En esta publicación, hemos aprendido sobre XML y cómo serializar datos en documentos XML, así como deserializar para extraer datos de documentos XML.

También hemos aprendido acerca de las anotaciones y cómo Jackson usa las anotaciones en el proceso de serialización y deserialización.

XML todavía se usa ampliamente en varios sistemas con los que podemos interactuar de vez en cuando, por lo tanto, para interactuar con ellos necesitaremos serializar y deserializar documentos XML de vez en cuando. También podemos consumir API XML en nuestros proyectos Java mientras exponemos puntos finales REST y usamos Jackson para convertir la entrada XML en salida JSON.

El código fuente de esta publicación está disponible en Github como referencia. .