Jackson Mixins: Ejemplos de casos de uso comunes

4 minuto(s) de lectura

Los mixins en Jackson son un mecanismo mediante el cual podemos añadir anotaciones a classes sin tener que modificar tales clases. Fue creado para aquellos casos en los que no podemos modificar una clase, como puede ser cuando trabajamos con clases de terceros.

Podemos utilizar cualquier anotación de Jackson pero no la añadimos directamente a la clase. En su lugar, las usamos en una clase mixin que puede ser una clase abstracta o una interface. Se pueden usar tanto para serialización o deserialización y se tienen que configurar en el ObjectMapper.

En las siguientes secciones vamos a ser bastante prácticos y vamos a mostrar algunos casos de uso donde los Jackson mixins nos pueden ser de utilidad.

Adaptando una clase de terceros para que sea serializable con Jackson

El primer caso que vamos a ver es cuando tenemos que usar una clase de terceros que no se puede serializar o deserializar con Jackson porque no sigue las convenciones establecidas. Como no podemos modificar estas clases, tenemos que usar mixins para añadir todo lo que Jackson necesita para llevar a cabo la serialización

Supongamos que queremos serializar la siguiente clase de terceros:

public class Person {

  private final String firstName;
  private final String lastName;

  public Person(String firstName, String lastName) {
    this.firstName = firstName ;
    this.lastName = lastName;
  }

  @Override
  public String toString() {
    return new StringJoiner(", ", Person.class.getSimpleName() + "[", "]")
        .add("firstName='" + firstName + "'")
        .add("lastName='" + lastName + "'")
        .toString();
  }
}

Jackson no puede serializar esta clase porque las propiedades son privadas y no hay getters ni setters. Por tanto, Jackson no reconocerá ninguna propiedad y lanzará una excepción:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.grabanotherbyte.jackson.Person and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

Vamos a crear ahora un mixin para solucionar este problema. En nuestro mixin añadiremos las propiedades que queremos serializar:

public abstract class PersonMixin {
  @JsonProperty private String firstName;
  @JsonProperty private String lastName;
}

En este caso, hemos creado una clase abstracta ya que más adelante también añadiremos un constructor.

A continuación, tenemos que decirle a Jackson que use nuestro mixin. Para hacer esto, tenemos que configurarlo en nuestro ObjectMapper:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.addMixIn(Person.class, PersonMixin.class);

En este momento, Jackson ya es capaz de serializar objetos de nuestra clase Person y serializará las propiedades firstName y lastName.

Por otro lado, si también queremos usar esta clase para deserializar tenemos que añadir a nuestro mixin un constructor adecuado para que Jackson sea capaz de crear instancias de esta clase:

public abstract class PersonMixin {
  // ... properties

  @JsonCreator
  public PersonMixin(@JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName) {}

}

Si no hiciésemos esto, Jackson lanzaría una excepción.

Ignorando propiedades

Consideremos ahora otro escenario que se nos puede dar cuando estamos serializando clases de terceros. Supongamos que ahora nuestra clase Person tiene todos los getters, setters y constructores necesarios y que se serializarán todas las propiedades de la clase.

public class Person {

  private String firstName;
  private String lastName;

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
}

Sin embargo, nos gustaría serializar solo el campo firstName y, como ocurría anteriormente, no podemos modificar esta clase. Al igual que antes, podemos solventar este problema utilizando un mixin:

public abstract class PersonMixin {
  @JsonIgnore private String lastName;
}

De esta manera Jackson igonrará esta propiedad y sólo serializará el firstName.

Cambiando los nombres de las propiedades

Siguiendo nuestros ejemplos anteriores, ahora nos gustaría cambiar el nombre de algunas propiedades cuando son serializadas.

Modifiquemos nuestro mixin para renombrar la propiedad lastName:

public abstract class PersonMixin {
  @JsonProperty("surname") private String lastName;
}

Ahora, nuestra propiedad lastName será serializada como surname.

Sobreescribiendo Serializers y Deserializers

Hay otros casos donde nos encontramos con clases que tienen sus propios serializers y deserializers pero queremos sobreescribirlos. Y, como anteriormente, no podemos o no queremos modificar estas clases.

Vamos a extender nuestra clase Person para incluir un serializer y un deserialzer propio:

@JsonSerialize(using = PersonSerializer.class)
@JsonDeserialize(using = PersonDeserializer.class)
public class Person {

  private final String firstName;
  private final String lastName;

  // getters, setters, constructors...

  public static class PersonSerializer extends JsonSerializer<Person> {

    static final String SEPARATOR = " ";

    @Override
    public void serialize(
        Person person, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
        throws IOException {
      jsonGenerator.writeString(person.getFirstName() + SEPARATOR + person.getLastName());
    }
  }

  public static class PersonDeserializer extends JsonDeserializer<Person> {

    @Override
    public Person deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
        throws IOException {
      String[] fields = jsonParser.getValueAsString().split(PersonSerializer.SEPARATOR);
      return new Person(fields[0], fields[1]);
    }
  }
}

Como podemos observar, ahora la clase Person es serializada concatenando el firstName y el lastName.

Sin embargo, en algunos casos queremos serializar esta clase de una forma distinta. Creemos otro serializer y deserializer para nuestra clase:

public static class PersonReversedSerializer extends JsonSerializer<Person> {

  static final String SEPARATOR = ", ";

  @Override
  public void serialize(
      Person person, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
      throws IOException {
    jsonGenerator.writeString(person.getLastName() + SEPARATOR + person.getFirstName());
  }
}

public static class PersonReversedDeserializer extends JsonDeserializer<Person> {

  @Override
  public Person deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
      throws IOException {
    String[] fields = jsonParser.getValueAsString().split(PersonReversedSerializer.SEPARATOR);
    return new Person(fields[1], fields[0]);
  }
}

En este momento, nuestra clase Person será serializada como “lastName, firstName”.

Como hemos hecho anteriormente, para usar estos nuevos serializers sin modificar nuestra clase Person tenemos que especificarlos en nuestro mixin:

@JsonSerialize(using = PersonReversedSerializer.class)
@JsonDeserialize(using = PersonReversedDeserializer.class)
public abstract class PersonMixin {}

Con esto, Jackson usará estos serializers y ignorará los que están especificados en la clase Person.

Para la serialización de una propiedad en concreto habría que seguir el mismo procedimiento.

Conclusión

En este tutorial, hemos introducido brevemente los mixins en Jackson para después centrarnos en algunos casos de uso donde pueden sernos de utilidad. Hemos mostrado algunos ejemplos para ilustrar estos casos y ver como funcionan estos mixins.

El código fuente de los ejemplos se puede encontrar en Github.

Categorías:

Actualizado:

Deja un comentario