Guía definitiva de Jackson ObjectMapper - Serializar y deserializar objetos Java

En esta guía detallada, aprenda todo lo que necesita saber sobre ObjectMapper. ¡Convierta JSON hacia y desde Java POJO, implemente deserializadores/serializadores personalizados y aprenda la diferencia entre @JsonProperty y @JsonAlias!

Introducción

Jackson es una biblioteca Java poderosa y eficiente que maneja la serialización y deserialización de objetos Java y sus representaciones JSON. Es una de las bibliotecas más utilizadas para esta tarea y se ejecuta bajo el capó de muchos otros marcos. Por ejemplo, aunque Spring Framework admite varias bibliotecas de serialización/deserialización, Jackson es el motor predeterminado.

En la era actual, JSON es, con mucho, la forma más común y preferida de producir y consumir datos mediante servicios web RESTFul, y el proceso es instrumental para todos los servicios web. Si bien Java SE no proporciona un amplio soporte para convertir JSON a objetos Java o viceversa, tenemos bibliotecas de terceros como Jackson para que se encarguen de esto por nosotros.

If you'd like to learn more about another useful Java library, Gson - read our guide to Convertir objeto Java (POJO) hacia y desde JSON con Gson!

Dicho esto, Jackson es una de las herramientas "debe conocer" para prácticamente todos los ingenieros de software de Java que trabajan en aplicaciones web, y estar familiarizado/cómodo con él lo ayudará a largo plazo.

En esta guía detallada, realizaremos una inmersión profunda en la API central de Jackson: ObjectMapper, brindándole una visión holística pero detallada de cómo puede usar la clase a través de muchos ejemplos prácticos. Luego, echaremos un vistazo al modelo de árbol para analizar estructuras arbitrarias, seguido de indicadores de personalización y escritura de serializadores y deserializadores personalizados.

Instalación de Jackson {#instalación de Jackson}

Empecemos por incluir a Jackson como una dependencia para nuestro proyecto. Si aún no tiene uno, puede generarlo fácilmente a través de CLI y Maven:

1
$ mvn archetype:generate -DgroupId=com.wikihtp.tutorial -DartifactId=objectmapper-tutorial -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false

O use Spring Initializr para crear un proyecto de esqueleto a través de una GUI. Jackson no es una dependencia integrada, por lo que no puede incluirlo de forma predeterminada ni desde la CLI ni desde Spring Initializr; sin embargo, incluirlo es tan fácil como modificar su archivo pom.xml con:

1
2
3
4
5
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.1</version>
</dependency>

O, si está utilizando Gradle como su herramienta de compilación:

1
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1'

Esto instala dos bibliotecas: jackson-annotations y jackson-core.

Introducción de la clase ObjectMapper

La clase principal en la biblioteca Jackson para leer y escribir JSON es ObjectMapper. Está en el paquete com.fasterxml.jackson.databind y puede serializar y deserializar dos tipos de objetos:

  1. Objetos Java simples y antiguos (POJO)
  2. Modelos de árbol JSON de uso general

Si ya tiene una clase de dominio, un POJO, puede convertir entre esa clase y JSON proporcionando la clase a ObjectMapper. Alternativamente, puede convertir cualquier JSON arbitrario en cualquier Modelo de árbol JSON arbitrario en caso de que no tenga una clase especializada para la conversión o si es "antieconómico" hacer una.

La clase ObjectMapper proporciona cuatro constructores para crear una instancia, siendo el siguiente el más simple:

1
ObjectMapper objectMapper = new ObjectMapper();

Estas son algunas de las características importantes de ObjectMapper:

  • Es seguro para subprocesos.
  • Sirve como fábrica para las clases ObjectReader y ObjectWriter más avanzadas.
  • El mapeador utilizará los objetos JsonParser y JsonGenerator para implementar la lectura y escritura real de JSON.

Los métodos disponibles en ObjectMapper son extensos, ¡así que comencemos!

Conversión de JSON a objetos Java

Podría decirse que una de las dos características más utilizadas es la conversión de JSON Strings a Java Objects. Esto normalmente se hace cuando recibe una respuesta que contiene una entidad serializada JSON y desea convertirla en un objeto para su uso posterior.

Con ObjectMapper, para convertir una cadena JSON en un Objeto Java, usamos el método readValue().

El método acepta una amplia variedad de fuentes de datos, que veremos en las próximas secciones.

Convertir cadena JSON en objeto Java (POJO)

La forma más simple de entrada es una ‘Cadena’, o más bien, Cadenas con formato JSON:

1
<T> T readValue(String content, Class<T> valueType)

Considere la siguiente clase HealthWorker en un Sistema de gestión de salud:

1
2
3
4
5
6
7
8
public class HealthWorker {
    private int id;
    private String name;
    private String qualification;
    private Double yearsOfExperience;

    // Constructor, getters, setters, toString()
}

Para convertir una representación de cadena JSON de esta clase en una clase Java, simplemente proporcionamos la cadena al método readValue(), junto con .class de la clase a la que estamos tratando de convertir:

1
2
3
4
ObjectMapper objectMapper = new ObjectMapper();
String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";

HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class);

Como era de esperar, la propiedad name del objeto healthWorker se establecería en "RehamMuzzamil", qualification en "MBBS" y yearsOfExperience en 1.5.

{.icon aria-hidden=“true”}

Nota: Los nombres de campo deben coincidir completamente con los campos en la cadena JSON, para que el mapeador no arroje un error. Además, deben tener getters y setters públicos válidos. Jackson también admite el uso de alias para diferentes nombres, que se pueden usar para asignar cualquier campo JSON a cualquier campo POJO con una simple anotación.

@JsonAlias ​​y @JsonProperty

Siempre que haya una discrepancia entre los nombres de propiedades/campos en una cadena JSON y un POJO, puede solucionar la discrepancia al no deserializarlos o "adaptar" qué campos JSON se asignan a qué campos de objetos.

Esto se puede lograr a través de @JsonAlias y @JsonProperty:

  • @JsonProperty corresponde a los nombres de campo durante la serialización y deserialización.
  • @JsonAlias corresponde a los nombres alternativos durante la deserialización.

Por ejemplo, ocurre una discrepancia común con las convenciones de uso de mayúsculas: una API puede devolver snake_case mientras espera CamelCase:

1
2
3
4
5
6
7
8
public class HealthWorker {
    private int workerId;
    private String workerName;
    private String workerQualification;
    private Double yearsOfExperience;
    
    // Constructor, getters, setters and toString()
}

Mientras que el JSON entrante se ve así:

1
2
3
4
5
6
{
  "worker_id" : 1,
  "worker_name" : "RehamMuzzamil",
  "worker_qualification" : "MBBS",
  "years_of_experience" :1.5
}

¡Todos estos serían campos no reconocidos, aunque obviamente representan las mismas propiedades! Esto se evita fácilmente configurando la anotación @JsonProperty:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class HealthWorker {
    @JsonProperty("worker_id")
    private int workerId;
    @JsonProperty("worker_name")
    private String workerName;
    @JsonProperty("worker_qualification")
    private String workerQualification;
    @JsonProperty("years_of_experience")
    private Double yearsOfExperience;
    
    // Constructor, getters, setters and toString()
}

Ahora, tanto al serializar como al deserializar, se aplicaría el caso de la serpiente y no surgirían problemas entre el POJO y el JSON entrante. Por otro lado, si no desea serializar los campos en mayúsculas y minúsculas, pero aun así puede leerlos, ¡puede optar por un alias en su lugar! El caso de serpiente entrante se analizaría en caso de camello, pero cuando se serializa, aún se serializaría en caso de camello.

¡Además, puede usar ambas anotaciones! En este contexto, @JsonAlias serviría como nombres alternativos para ser aceptados además del nombre de propiedad obligatorio, e incluso puede proporcionar una lista a la anotación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class HealthWorker {

    @JsonProperty("worker_id")
    @JsonAlias({"id", "workerId", "identification"})
    private int workerId;
    @JsonProperty("worker_name")
    @JsonAlias({"name", "wName"})
    private String workerName;
    @JsonProperty("worker_qualification")
    @JsonAlias({"workerQualification", "qual", "qualification"})
    private String workerQualification;
    @JsonProperty("years_of_experience")
    @JsonAlias({"yoe", "yearsOfExperience", "experience"})
    private Double yearsOfExperience;
    
    // Constructor, getters, setters and toString()
}

Ahora, cualquiera de los alias se asignaría a la misma propiedad, pero al serializar, se usaría el valor @JsonProperty. Podría asignar varias respuestas de API a un solo objeto de esta manera, si las API contienen la misma respuesta estructural, por ejemplo, con nombres diferentes.

Convertir cadena JSON en objeto Java (POJO) con lectores

Una clase Reader representa un flujo de datos de caracteres arbitrarios y se puede construir a partir de fuentes como Strings. El método readValue() también acepta un Reader en lugar de Strings:

1
<T> T readValue(Reader src, Class<T> valueType)

El resto del código es muy similar:

1
2
3
4
ObjectMapper objectMapper = new ObjectMapper();
String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
Reader reader = new StringReader(healthWorkerJSON);
HealthWorker healthWorker = objectMapper.readValue(reader, HealthWorker.class);

Convertir archivo JSON en objeto Java (POJO)

JSON no solo viene en formato de cadena; a veces, se almacena en un archivo. JSON se puede usar para formatear las propiedades de un archivo de configuración (que se puede cargar en un objeto de configuración para establecer el estado de la aplicación), por ejemplo.

La función readValue() puede asignar datos JSON de un archivo directamente a un objeto, aceptando también un Archivo:

1
<T> T readValue(File src, Class<T> valueType)

La API no cambia mucho: carga el archivo y lo pasa al método readValue():

1
2
3
ObjectMapper objectMapper = new ObjectMapper();
File file = new File("<path-to-file>/HealthWorker.json");
HealthWorker healthWorker = objectMapper.readValue(file, HealthWorker.class);

{.icon aria-hidden=“true”}

Nota: Esto funciona igual si usa un objeto FileReader en lugar de un objeto File.

Convertir JSON a objeto Java (POJO) desde la respuesta HTTP/URL

JSON se creó para ser un formato de intercambio de datos, especialmente para aplicaciones web. Una vez más, es el formato más frecuente para la serialización de datos en la web. Si bien puede recuperar el resultado, guárdelo como una cadena y luego conviértalo usando el método readValue (); puede leer directamente la respuesta HTTP, dada una URL, y deserializarla a la clase deseada:

1
<T> T readValue(URL src, Class<T> valueType)

Con este enfoque, puede omitir la cadena intermedia y analizar directamente los resultados de la solicitud HTTP.

Consideremos un Sistema de Gestión de Pronósticos del Tiempo en el que nos basamos en los datos compartidos por un servicio web del Departamento Meteorológico:

1
2
3
4
5
String API_KEY = "552xxxxxxxxxxxxxxxxx122&";
String URLString = "http://api.weatherapi.com/v1/astronomy.json?key="+API_KEY+"q=London&dt=2021-12-30\n";
URL url = new URL(URLString); // Create a URL object, don't just use a URL as a String
ObjectMapper objectMapper = new ObjectMapper();
Astronomy astronomy = objectMapper.readValue(url, Astronomy.class);

Aquí hay una instantánea de lo que contendrá nuestro objeto astronomía:

Salida del mapeo de datos JSON de URL a Objeto Java

Nuevamente, la clase Astronomy solo refleja la estructura JSON esperada.

Convertir flujo de entrada JSON en objeto Java (POJO)

El InputStream representa cualquier flujo arbitrario de bytes, y no es un formato poco común para recibir datos. Naturalmente, ObjectMapper también puede leer un InputStream y asignar los datos entrantes a una clase de destino:

1
<T> T readValue(InputStream src, Class<T> valueType)

Por ejemplo, vamos a convertir datos JSON de un FileInputStream:

1
2
3
ObjectMapper objectMapper = new ObjectMapper();
InputStream inputStream = new FileInputStream("<path-to-file>/HealthWorker.json");
HealthWorker healthWorker = objectMapper.readValue(inputStream, HealthWorker.class);

Convertir matriz de bytes JSON en objeto Java (POJO)

Los JSON Byte Arrays se pueden usar para almacenar datos, más comúnmente como blobs (por ejemplo, una base de datos relacional como PostgreSQL o MySQL). En otro tiempo de ejecución, ese blob se recupera y se deserializa nuevamente en un objeto. El tipo de datos BLOB es de particular importancia ya que es comúnmente utilizado por una variedad de aplicaciones, incluidos los intermediarios de mensajes, para almacenar la información binaria de un archivo.

Diagrama de flujo de Serialización y Deserialización con tipo de datos Blob

El método readValue() de la clase ObjectMapper también se puede usar para leer matrices de bytes:

1
<T> T readValue(byte[] src, Class<T> valueType)

Si tiene datos JSON como una matriz de bytes (byte[]), los mapeará como lo haría normalmente:

1
2
3
4
5
ObjectMapper objectMapper = new ObjectMapper();
String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
// Ensure UTF-8 format
byte[] jsonByteArray = healthWorkerJSON.getBytes("UTF-8");
HealthWorker healthWorker = objectMapper.readValue(jsonByteArray, HealthWorker.class);

Convertir JSON Array a Java Object Array o List

Leer datos de una matriz JSON y convertirlos en una matriz o lista de objetos Java es otro caso de uso: no solo busca recursos únicos. Utiliza la misma firma que la lectura de un solo objeto:

1
<T> T readValue(String content, TypeReference<T> valueTypeRef)

Siempre que el JSON contenga una matriz, podemos asignarlo a una matriz de objetos:

1
2
3
4
5
String healthWorkersJsonArray = "[{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5},{\"id\":2,\"name\":\"MichaelJohn\",\"qualification\":\"FCPS\",\"yearsOfExperience\":5}]";
ObjectMapper objectMapper = new ObjectMapper();
HealthWorker[] healthWorkerArray = objectMapper.readValue(healthWorkersJsonArray, HealthWorker[].class);
// OR
HealthWorker[] healthWorkerArray = objectMapper.readValue(jsonKeyValuePair, new TypeReference<HealthWorker[]>(){});

Sin embargo, dado que las matrices son complicadas para trabajar, puede convertir fácilmente la matriz JSON en una Lista de objetos:

1
2
3
String healthWorkersJsonArray = "[{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5},{\"id\":2,\"name\":\"MichaelJohn\",\"qualification\":\"FCPS\",\"yearsOfExperience\":5}]";
ObjectMapper objectMapper = new ObjectMapper();
List<HealthWorker> healthWorkerList = objectMapper.readValue(healthWorkersJsonArray, new TypeReference<List<HealthWorker>(){});

Convertir cadena JSON en mapa Java

La clase Map se utiliza para almacenar pares clave-valor en Java. Los objetos JSON son pares clave-valor, por lo que el mapeo de uno a otro es un ajuste natural.

1
<T> T readValue(String content, TypeReference<T> valueTypeRef)

Podemos convertir datos JSON en un objeto ‘Mapa’, con la clave JSON correspondiente a la clave del mapa, y el valor de JSON correspondiente al valor del mapa tan fácilmente como:

1
2
3
4
5
String jsonKeyValuePair = "{\"TeamPolioVaccine\":10,\"TeamMMRVaccine\":19}";
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> jsonMap = objectMapper.readValue(jsonKeyValuePair, new TypeReference<HashMap>(){});
// OR
Map<String, Object> jsonMap = objectMapper.readValue(jsonKeyValuePair, HashMap.class);

Este Mapa contendría:

1
{TeamPolioVaccine=10, TeamMMRVaccine=19}

Convertir objetos Java (POJO) a JSON

Hemos visto muchas formas y fuentes de entrada que pueden representar datos JSON y cómo convertir esos datos en una clase Java predefinida. ¡Ahora, giremos la palanca al revés y echemos un vistazo a cómo serializar objetos Java en datos JSON!

Similar a la conversión inversa: el método writeValue() se usa para serializar objetos Java en JSON.

Puede escribir objetos en una cadena, un archivo o un flujo de salida.

Convertir objeto Java en cadena JSON

Nuevamente, la forma más simple en la que se puede serializar su objeto es una cadena con formato JSON:

1
String writeValueAsString(Object value)

Alternativamente, y más raramente, puede escribirlo en un archivo:

1
void writeValue(File resultFile, Object value)

Hay menos variedad aquí, ya que la mayor parte de la variedad puede surgir en el extremo receptor. Escribamos un HealthWorker en JSON:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
ObjectMapper objectMapper = new ObjectMapper();
HealthWorker healthWorker = createHealthWorker();
// Write object into a File
objectMapper.writeValue(new File("healthWorkerJsonOutput.json"),healthWorker);
// Write object into a String
String healthWorkerJSON = objectMapper.writeValueAsString(healthWorker);
System.out.println(healthWorkerJSON);

private static HealthWorker createHealthWorker() {
    HealthWorker healthWorker = new HealthWorker();
    healthWorker.setId(1);
    healthWorker.setName("Dr. John");
    healthWorker.setQualification("FCPS");
    healthWorker.setYearsOfExperience(5.0);
    return healthWorker;
}

healthWorkerJsonOutput.json se creó en el directorio actual con los siguientes contenidos:

1
2
3
4
5
6
{
  "id": 1,
  "name": "Dr. John",
  "qualification": "FCPS",
  "yearsOfExperience": 5.0
}

Convertir objeto Java en FileOutputStream

Al guardar objetos en un archivo JSON, el contenido se convierte internamente en un FileOutputStream antes de guardarse, y puede usar un OuputStream directamente en su lugar:

1
void writeValue(OutputStream out, Object value)

La API funciona de la misma manera que se vio anteriormente:

1
2
3
ObjectMapper objectMapper = new ObjectMapper();
HealthWorker healthWorker = createHealthWorker();
objectMapper.writeValue(new FileOutputStream("output-health-workers.json"), healthWorker);

Esto daría como resultado un archivo, output-health-workers.json, que contiene:

1
2
3
4
5
6
{
  "id": 1,
  "name": "Dr. John",
  "qualification": "FCPS",
  "yearsOfExperience": 5.0
}

Modelo de árbol JSON de Jackson - Estructuras JSON desconocidas

Un objeto JSON se puede representar utilizando el modelo de árbol incorporado de Jackson en lugar de clases predefinidas también. El modelo de árbol de Jackson es útil cuando no sabemos cómo se verá el JSON receptor o no podemos diseñar una clase para representarlo de manera efectiva.

Descripción general de JsonNode

JsonNode es una clase base para todos los nodos JSON, que constituye la base del JSON Tree Model de Jackson. Reside en el paquete com.fasterxml.jackson.databind.JsonNode.

Jackson puede leer JSON en una instancia de JsonNode y escribir JSON en JsonNode utilizando la clase ObjectMapper. Por definición, JsonNode es una clase abstracta que no se puede instanciar directamente. Sin embargo, hay 19 subclases de JsonNode que podemos usar para crear objetos.

Convertir objeto Java en JsonNode usando ObjectMapper

La clase ObjectMapper proporciona dos métodos que vinculan datos de un objeto Java a un árbol JSON:

1
<T extends JsonNode> T valueToTree(Object fromValue)

Tanto como:

1
<T> T convertValue(Object fromValue, Class<T> toValueType)

En esta guía usaremos valueToTree(). Es similar a serializar valores en JSON, pero es más eficiente. El siguiente ejemplo demuestra cómo podemos convertir un objeto en un JsonNode:

1
2
3
4
ObjectMapper objectMapper = new ObjectMapper();
HealthWorkerService healthWorkerService = new HealthWorkerService();
HealthWorker healthWorker = healthWorkerService.findHealthWorkerById(1);
JsonNode healthWorkerJsonNode = objectMapper.valueToTree(healthWorker);

Convertir JsonNode en objeto mediante ObjectMapper

La clase ObjectMapper también proporciona dos métodos convenientes que vinculan datos de un árbol JSON a otro tipo (típicamente un POJO):

1
<T> T treeToValue(TreeNode n, Class<T> valueType)

Y:

1
<T> T convertValue(Object fromValue, Class<T> toValueType)

En esta guía usaremos treeToValue(). El siguiente código demuestra cómo puede convertir JSON en un objeto, convirtiéndolo primero en un objeto JsonNode:

1
2
3
4
5
String healthWorkerJSON = "{\n\t\"id\": null,\n\t\"name\": \"Reham Muzzamil\",\n\t\"qualification\": \"MBBS\",\n\t\"yearsOfExperience\": 1.5\n}";
ObjectMapper objectMapper = new ObjectMapper();

JsonNode healthWorkerJsonNode = objectMapper.readTree(healthWorkerJSON);
HealthWorker healthWorker = objectMapper.treeToValue(healthWorkerJsonNode, HealthWorker.class);

Configuración de la serialización y deserialización de ObjectMapper {#configuración de serialización y deserialización de ObjectMapper}

El JSON de entrada puede diferir o ser incompatible con el POJO de destino según la técnica de deserialización predeterminada de la API de Jackson. Aquí están algunos ejemplos:

  • Los campos de una cadena JSON no están disponibles en el POJO asociado.
  • En una cadena JSON, los campos de tipos primitivos tienen valores nulos.

Ambos casos son muy comunes y, por lo general, querrá poder lidiar con ellos. ¡Afortunadamente, ambos son fáciles de recuperar! También hay situaciones en las que queremos gestionar la personalización a lo largo del proceso de serialización, como

  • Use formato textual para serializar objetos de Fecha en lugar de marcas de tiempo.
  • Controlar el comportamiento del proceso de serialización cuando no se encuentran accesores para un tipo particular.

En estos casos, podemos configurar el objeto ObjectMapper para cambiar su comportamiento. El método configure() nos permite cambiar los métodos de serialización y deserialización predeterminados:

1
2
ObjectMapper configure(SerializationFeature f, boolean state)
ObjectMapper configure(DeserializationFeature f, boolean state)

Hay una extensa lista de propiedades, y vamos a echar un vistazo a las más pertinentes. Todos tienen valores predeterminados razonables: no tendrá que cambiarlos en la mayoría de los casos, pero en circunstancias más específicas, es muy útil saber cuáles puede cambiar.

FAIL_ON_EMPTY_BEANS

La función de serialización FAIL_ON_EMPTY_BEANS define lo que sucede cuando no se encuentran accesores (propiedades) para un tipo. Si está habilitado (valor predeterminado), se lanza una excepción para indicar que el bean no es serializable. Si está deshabilitado, un bean se serializa como un objeto vacío sin propiedades.

Querremos deshabilitar la función en escenarios como cuando una clase solo tiene importaciones relacionadas con la configuración y no tiene campos de propiedad, pero en algunos casos, esta excepción puede "hacerle tropezar" si está trabajando con un objeto sin métodos/propiedades públicas, lo que da como resultado una excepción no deseada.

Consideremos una clase Java vacía:

1
class SoftwareEngineer {}

La clase ObjectMapper lanza la siguiente excepción cuando intenta serializar una clase sin propiedades:

1
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.wikihtp.tutorial.SoftwareEngineer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

En el contexto de este escenario, deshabilitar la función es útil para procesar la serialización sin problemas. El siguiente fragmento de código muestra cómo deshabilitar esta propiedad de serialización:

1
2
3
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
System.out.println(objectMapper.writeValueAsString(new SoftwareEngineer()));

La ejecución del fragmento de código anterior da como resultado un objeto vacío.

1
{}

WRITE_DATES_AS_TIMESTAMPS

Las fechas se pueden escribir en una miríada de formatos, y el formato de las fechas difiere de un país a otro. La función WRITE_DATES_AS_TIMESTAMPS define si desea escribir el campo de fecha como una marca de tiempo numérica o como otro tipo.

De forma predeterminada, la función se establece en “verdadero”, ya que es una forma muy universal de representar una fecha, y la miríada de formatos antes mencionada se puede derivar más fácilmente de una marca de tiempo que de otros formatos. Alternativamente, es posible que desee forzar un formato más fácil de usar:

1
2
3
4
5
6
7
8
9
Date date = Calendar.getInstance().getTime();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String dateString = dateFormat.format(date);
System.out.println(dateString);

ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(date));
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
System.out.println(objectMapper.writeValueAsString(date));

Ejecutar el código anterior nos daría este resultado:

1
2
3
2022-01-01 08:34:55
1641051295217
"2022-01-01T15:34:55.217+00:00"

FALLO_EN_PROPIEDADES_DESCONOCIDAS

Si la cadena JSON contiene campos que no son familiares para POJO, ya sea un solo campo String o más, el proceso de deserialización genera una UnrecognizedPropertyException. ¿Qué pasa si no nos importa capturar todos los campos de datos?

Cuando trabaje con API de terceros, puede esperar que las respuestas JSON cambien con el tiempo. Por lo general, estos cambios no se anuncian, por lo que una nueva propiedad podría aparecer silenciosamente y rompería su código. La solución es fácil: simplemente agregue la nueva propiedad a su POJO. Sin embargo, en algunos casos, esto implicaría actualizar otras clases, DTO, clases de recursos, etc. solo porque un tercero agregó una propiedad que podría no ser relevante para usted.

Es por eso que FAIL_ON_UNKNOWN_PROPERTIES se establece en falso por defecto, y Jackson simplemente ignorará las nuevas propiedades si están presentes.

Por otro lado, es posible que desee forzar la solidaridad de respuesta dentro de un proyecto, para estandarizar los datos que se transmiten entre las API, en lugar de que Jackson ignore silenciosamente las propiedades si se modifican (erróneamente). Esto le “avisará” sobre cualquier cambio que se esté realizando:

1
2
3
ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
String healthWorkerJsonUpdated = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5,\"specialization\":\"Peadiatrics\"}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJsonUpdated, HealthWorker.class);

El código anterior introduce una especialización de propiedad desconocida en la cadena JSON. Ejecutarlo daría como resultado la siguiente excepción:

1
Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "specialization" (class com.wikihtp.model.HealthWorker), not marked as ignorable (4 known properties: "id", "qualification", "name", "yearsOfExperience"])

{.icon aria-hidden=“true”}

Nota: establecer esta propiedad en true afectaría a todos los POJO creados por la instancia de ObjectMapper. Para evitar esta configuración más "global", podemos agregar esta anotación a nivel de clase: @JsonIgnoreProperties(ignoreUnknown = true).

FALLA_EN_NULO_PARA_PRIMITIVAS

La función FAIL_ON_NULL_FOR_PRIMITIVES determina si se produce un error al encontrar propiedades JSON como null mientras se deserializa en tipos primitivos de Java (como int o double). De forma predeterminada, los valores nulos para los campos primitivos se ignoran. Sin embargo, podemos configurar ObjectMapper para que falle, en el caso de que la omisión de esos campos indique un error mayor.

El siguiente código habilita esta función de deserialización:

1
2
3
4
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);
String healthWorkerJSON = "{\"id\":null,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class);

Esto daría como resultado:

1
Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot map `null` into type `int` (set DeserializationConfig.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES to 'false' to allow)

ACCEPT_EMPTY_STRING_AS_NULL_OBJECT

Cuando queremos permitir o prohibir que los valores de cadena vacíos de JSON "" se vinculen a POJO como null, podemos configurar esta propiedad. De forma predeterminada, esta función está activada.

Para demostrar el uso de esta función de deserialización, hemos modificado nuestra clase HealthWorker de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class HealthWorker {

    private int id;
    private String name;
    private String qualification;
    private Double yearsOfExperience;
    private Specialization specialization;

    // Constructor, getters, setters, toString()
}

Ahora tiene una propiedad llamada “especialización”, que se define como:

1
2
3
4
5
public class Specialization {
    private String specializationField;

    // Constructor, getters, setters, toString()
}

Mapeemos algún JSON de entrada a un objeto HealthWorker:

1
2
3
4
5
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
String healthWorkerJSON = "{\"id\":1,\"name\":\"\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5,\"specialization\":\"\"}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class);
System.out.println(healthWorker.getSpecialization());

Esto resulta en:

1
null

Cree un serializador y deserializador personalizado con Jackson {#cree un serializador y deserializador personalizado con jackson}

Anteriormente, encontramos una discrepancia entre los campos de cadena JSON y los campos de objeto Java, que se "adaptan" fácilmente entre sí a través de anotaciones. Sin embargo, a veces, el desajuste es estructural, no semántico.

La clase ObjectMapper le permite registrar un serializador o deserializador personalizado para estos casos. Esta característica es útil cuando la estructura JSON es diferente a la clase POJO de Java en la que debe serializarse o deserializarse.

¿Por qué? Bueno, es posible que desee utilizar datos de JSON o de clase como un tipo diferente. Por ejemplo, una API puede proporcionar un número, pero en su código le gustaría trabajar con él como una cadena.

Antes de que pudiéramos personalizar fácilmente los serializadores y deserializadores, era común que los desarrolladores usaran Objetos de transferencia de datos (DTO), clases para interactuar con la API, que luego se usarían para completar nuestros POJO:

Conversión de DTO en la era temprana

If you'd like to read more about DTOs - read our Guía para el patrón de objetos de transferencia de datos en Java: implementación y mapeo!

Los serializadores personalizados nos permiten omitir ese paso. ¡Vamos a sumergirnos!

Implementación de un serializador personalizado de Jackson

Implementemos algunos serializadores para tener una idea de cómo se pueden usar. Este serializador toma un valor DateTime nativo y lo formatea en una cadena compatible con el lector/API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class CustomJodaDateTimeSerializer extends StdSerializer<DateTime> {

    private static DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");

    public CustomJodaDateTimeSerializer() {
        this(null);
    }

    public CustomJodaDateTimeSerializer(Class<DateTime> t) {
        super(t);
    }

    @Override
    public void serialize(DateTime value, JsonGenerator jsonGenerator, SerializerProvider arg2) throws IOException {
        jsonGenerator.writeString(formatter.print(value));
    }
}

Este serializador convierte un valor doble (por ejemplo, un precio en dólares y centavos) en una cadena:

1
2
3
4
5
6
7
public class DoubleToStringCustomSerializer extends JsonSerializer<Double> {

    @Override
    public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(value.toString());
    }
}

Este serializador devuelve un objeto JSON basado en los datos de un objeto HealthWorker. Tenga en cuenta el cambio de la propiedad ’nombre’ del objeto Java y el ’nombre_completo’ del JSON:

 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
public class HealthWorkerCustomSerializer extends StdSerializer<HealthWorker> {

    private static final long serialVersionUID = 1L;

    public HealthWorkerCustomSerializer() {
        this(null);
    }

    public HealthWorkerCustomSerializer(Class clazz) {
        super(clazz);
    }

    @Override
    public void serialize(HealthWorker healthWorker, JsonGenerator jsonGenerator, SerializerProvider serializer)
    throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeNumberField("id", healthWorker.getId());
        jsonGenerator.writeStringField("full_name",
        healthWorker.getName());
        jsonGenerator.writeStringField("qualification", healthWorker.getQualification());
        jsonGenerator.writeObjectField("yearsOfExperience", healthWorker.getYearsOfExperience());
        jsonGenerator.writePOJOField("dateOfJoining", healthWorker.getDateOfJoining());
        jsonGenerator.writeEndObject();
    }
}

Supongamos que podemos recuperar datos de trabajadores de la salud con un objeto HealthWorkerService, que aprovecharía un servicio web para encontrar un trabajador de la salud por ID. Así es como puede configurar serializadores personalizados como los que creamos anteriormente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();

simpleModule.addSerializer(DateTime.class, new CustomJodaDateTimeSerializer());
simpleModule.addSerializer(Double.class, new DoubleToStringCustomSerializer());
simpleModule.addSerializer(HealthWorker.class, new HealthWorkerCustomSerializer());
objectMapper.registerModule(simpleModule);

HealthWorkerService healthWorkerService = new HealthWorkerService();
HealthWorker healthWorker = healthWorkerService.findHealthWorkerById(1);
String healthWorkerCustomSerializedJson = objectMapper.writeValueAsString(healthWorker);
System.out.println(healthWorkerCustomSerializedJson);

Observe cómo se agregan serializadores a un módulo, que luego es registrado por ObjectMapper:

1
2
3
4
5
6
7
{
  "id": 1,
  "full_name": "Dr. John",
  "qualification": "FCPS",
  "yearsOfExperience": "5.0",
  "dateOfJoining": "2022-01-02 00:28"
}

Aquí podemos observar que el campo name se modifica a full_name, que el valor de yearsOfExperience se devuelve como "5.0", que es un valor de cadena, y que el valor de dateOfJoining se devuelve según el formato definido.

Implementación de un deserializador Jackson personalizado {#implementingajacksondeserializer personalizado}

La siguiente implementación de un deserializador personalizado agrega un valor al nombre:

 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
public class HealthWorkerCustomDeserializer extends StdDeserializer {

    private static final long serialVersionUID = 1L;

    public HealthWorkerCustomDeserializer() {
        this(null);
    }

    public HealthWorkerCustomDeserializer(Class clazz) {
        super(clazz);
    }

    @Override
    public HealthWorker deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        HealthWorker healthWorker = new HealthWorker();
        JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
        JsonNode customNameNode = jsonNode.get("name");
        JsonNode customQualificationNode = jsonNode.get("qualification");
        JsonNode customYearsOfExperienceNode = jsonNode.get("yearsOfExperience");
        JsonNode customIdNode = jsonNode.get("yearsOfExperience");
        String name = "Dr. " + customNameNode.asText();
        String qualification = customQualificationNode.asText();
        Double experience = customYearsOfExperienceNode.asDouble();
        int id = customIdNode.asInt();
        healthWorker.setName(name);
        healthWorker.setQualification(qualification);
        healthWorker.setYearsOfExperience(experience);
        healthWorker.setId(id);
        return healthWorker;
    }
}

Agregar un deserializador es similar a agregar un serializador, se agregan a módulos que luego se registran en la instancia ObjectMapper:

1
2
3
4
5
6
7
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(HealthWorker.class, new HealthWorkerCustomDeserializer());
objectMapper.registerModule(simpleModule);
String healthWorkerJSON = "{\n\t\"id\": 1,\n\t\"name\": \"Reham Muzzamil\",\n\t\"qualification\": \"MBBS\",\n\t\"yearsOfExperience\": 1.5\n}";
HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON,HealthWorker.class);
System.out.println(healthWorker.getName());

Ejecutar este código producirá este resultado:

1
Dr. Reham Muzzamil

Como podemos ver en la salida, Dr. se agrega al nombre del trabajador de la salud según la lógica de deserialización personalizada.

Conclusión

Esto nos lleva a la conclusión de la guía. Hemos cubierto la clase ObjectMapper: la API central de Jackson para la serialización y deserialización de objetos Java y datos JSON.

Primero echamos un vistazo a cómo instalar Jackson y luego nos sumergimos en la conversión de JSON a objetos Java, desde cadenas, archivos, respuestas HTTP, flujos de entrada y matrices de bytes. Luego exploramos la conversión de JSON a listas y mapas de Java.

Hemos cubierto las anotaciones @JsonProperty y @JsonAlias para los nombres de campo que no coinciden con "puente", antes de convertir objetos Java en datos JSON.

Cuando no conoce la estructura del JSON entrante por adelantado, ¡puede usar la clase genérica JsonNode para guardar los resultados!

Con el uso general fuera del camino, exploramos algunos de los indicadores de personalización, que modifican el comportamiento de ObjectMapper, e incluso implementamos varios serializadores y deserializadores propios.