Deserialización con Jackson usando el patrón de diseño Builder

5 minuto(s) de lectura

El patrón de diseño Builder es uno de esos patrones que son ampliamente utilizados por los desarrolladores. Es muy útil para crear clases que tienen muchas propiedades y/o cuando algunas de ellas son opcionales. Cuando trabajamos con builders suele ser bastante común que los constructores de nuestras clases sean privados y que solo se puedan crear instancias de las mismas a través de su builder. Por este motivo, si usamos estas clases para deserializar con Jackson puede que nos encontremos con problemas, ya que Jackson no puede crear instancias de nuestras clases debido a que no tienen un constructor público.

En este tutorial vamos a mostrar como usar nuestro builders para deserializar JSON con Jackson. También mostraremos como hacer esto si utilizamos Lombok para generar nuestros builders.

Deserialización con Jackson usando un builder

Lo primero que vamos a hacer es crear una clase de ejemplo que utilizaremos durante el resto del tutorial.

Vamos a crear una clase Address que simplemente modela una dirección:

public class Address {

  private final String street;
  private final String zipCode;
  private final String city;
  private final String province;
  private final String country;

  private Address(String street, String zipCode, String city, String province, String country) {
    this.street = street;
    this.zipCode = zipCode;
    this.city = city;
    this.province = province;
    this.country = country;
  }

  // ... getters, equals, hashCode and toString
  
  public static class Builder {

    private String street;
    private String zipCode;gvkh
    private String city;
    private String province;
    private String country;

    public Builder street(String street) {
      this.street = street;
      return this;
    }

    public Builder zipCode(String zipCode) {
      this.zipCode = zipCode;
      return this;
    }

    public Builder city(String city) {
      this.city = city;
      return this;
    }

    public Builder province(String province) {
      this.province = province;
      return this;
    }

    public Builder country(String country) {
      this.country = country;
      return this;
    }

    public Address build() {
      return new Address(street, zipCode, city, province, country);
    }
  }
}

Como podemos ver, esta clase es inmutable y sólo se pueden crear instancias de ella a través de su builder.

Si intentamos deserializar un JSON en nuestra clase Address, Jackson lanzará una excepción diciendo que no puede encontrar un constructor adecuado para crear instancias de nuestra clase:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.grabanotherbyte.jackson.builder.Address` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"street":"street","zipCode":"1234","city":"my city","province":"province","country":"country"}"; line: 1, column: 2]

Esto ocurre porque el constructor de nuestra clase es privado.

Lo que tenemos que hacer es decirle a Jackson que utilice nuestro builder para la deserialización de la clase. Podemos hacer esto anotando nuestra clase con @JsonDeserialize y especificandole nuestra clase Builder:

@JsonDeserialize(builder = Address.Builder.class)
public class Address {
  // ... rest of the class
}

Sin embargo, todavía obtendremos otra excepción si tratamos de usar esta clase tal y como la tenemos ahora mismo:

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "street" (class com.grabanotherbyte.jackson.builder.Address$Builder), not marked as ignorable (0 known properties: ])
 at [Source: (String)"{"street":"street","zipCode":"1234","city":"my city","province":"province","country":"country"}"; line: 1, column: 12] (through reference chain: com.grabanotherbyte.jackson.builder.Address$Builder["street"])

Ahora Jackson nos dice que no puede reconocer las propiedades de nuestra clase Builder. Esto ocurre porque Jackson espera que nuestro builder utilice el perfijo with en los métodos setter. Y en nuestro caso no utilizamos ningún prefijo.

Una opción para solucionar esto es cambiar nuestro builder para usar el prefijo with. Otra opción es indicarle a Jackson que use un prefijo diferente a través de la anotación @JsonPOJOBuilder en nuestra clase Builder:

@JsonPOJOBuilder(withPrefix = "")
public static class Builder {
  // ... rest of the class
}

En este caso, hemos dejado el prefijo como un string vacío ya que no usamos ningún prefijo.

Además, es importante tener en cuenta que Jackson espera que el métodod que crea instancias en nuestro builder se llame build. Si queremos utilizar otro nombre lo tenemos que especificar en esta misma anotación:

@JsonPOJOBuilder(withPrefix = "", buildMethodName = "create")
public static class Builder {
  // ... rest of the class
}

En este ejemplo hemos supuesto que el método de nuestro builder se llama create.

Deserializando usando Lombok para generar nuestros builders

Vamos a hacer ahora lo mismo que en la sección anterior pero trabajando con una clase que usa Lombok para generar su builder.

Primero, vamos a modificar nuestra clase Addresss para añadir las anotaciones de Lombok necesarias:

@Value
@Builder(setterPrefix = "with")
public class Address {

  String street;
  String zipCode;
  String city;
  String province;
  String country;

}

Esta clase también es inmutable y tiene constructor privado. Por tanto, sólo se pueden crear instancias de la misma a través de su builder.

Observemos también que nuestro builder ahora usa el prefijo with en los métodos setter. Por tanto, simplemente necesitamos decirle a Jackson que utilice nuestra clase Builder y ya estaríamos listos:

@Value
@Builder(setterPrefix = "with")
@JsonDeserialize(builder = AddressLombok.AddressBuilder.class)
public class Address {
  // ... rest of the class
}

Esto es suficiente para utilizar esta clase con Jackson a la hora de deserializar.

Sin embargo, imaginémonos que no queremos usar el prefijo with en nuestro builder, ¿cómo podemos decirle a Jackson que utilize otro prefijo? Para ello, tenemos que declarar la clase Builder explícitamente y usar la anotación @JsonPOJOBuilder como hicimos anteriormente:

@Value
@Builder
@JsonDeserialize(builder = Address.AddressBuilder.class)
public class Address {

  String street;
  String zipCode;
  String city;
  String province;
  String country;

  @JsonPOJOBuilder(withPrefix = "")
  public static class AddressBuilder {
    // Lombok añadirá el resto
  }
}

Es importante mencionar que Lombok usa build como el nombre por defecto para el métodod que crea las instancias en los builders. Si queremos utilizar un nombre distinto tenemos que especifiarlo en la propiedad buildMethodName de la anotación @JsonPOJOBuilder como hicimos en la sección anterior.

@Jacksonized

En la versión 1.18.14 Lombok ha introducido una funcionalidad experimental llamada @Jacksonized que pretende simplificar la integración entre Lombok y Jackson.

Modifiquemos ahora nuestro ejemplo anterior para usar estar anotación. Es tan sencillo como reemplazar todas nuestras anotaciones de Jackson con @Jacksonized:

@Value
@Jacksonized
@Builder
public class Address {

  String street;
  String zipCode;
  String city;
  String province;
  String country;

}

¡Ya hemos acabado! ¡Nuestra clase está lista!

Además, si cambiamos el prefijo de los métodos setter o el nombre del método buid de nuestro builder, Lombok se encargará de modificar las anotaciones generadas de Jackson en consecuencia.

Esta anotación es muy útil y nos permite ahorrarnos código y tiempo a la hora de trabajar con builders y Jackson. Pero recordemos que, en el momento de escribir este tutorial, esta anotación todavía es experimental.

Conclusión

En este tutorial hemos explicado como utilizar Jackson para deserializar clases que utilizan el patrón de diseño Builder. Hemos mostrado ejemplos de como hacerlo y también hemos enseñado como personalizar el comportamiento por defecto en las anotaciones de Jackson. Por último, también hemos incluido como hacer esto mismo pero utilizando la librería Lombok para generar nuestros builders.

El código fuente de los ejemplos se encuentra disponible en Github.

Categorías:

Actualizado:

Deja un comentario