Serialización y Deserialización en Herencia con Jackson

Serialización y Deserialización en Herencia con Jackson

Esta entrada de Refactorizando sobre Serialización y Deserialización en Herencia con Jackson es la continuación del artículo anterior sobre Serialización y Deserialización con Jackson.

Como ya comentamos en el anterior artículo, Jackson es una librería que nos va a ayudar a serliazar y deserializar objetos para poder mapearlos a mi clase. En este artículo veremos que es lo que sucede cuando tenemos herencia.

Vamos a ver dos tipos de herencia, cuando queremos excluir información o cuando queremos añadir.

En este artículo usaremos Lombok, en algunos de nuestros ejemplos.

A continuación vamos a ver ejemplos con inclusión y exclusión.

Dependencias Maven con Jackson

   <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.12.1</version>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.1</version>
    </dependency>

Inclusión de información en Serialización y Deserialización

Haciendo uso de ObjectMapper de manera global

Para nuestros ejemplos vamos a usar una clase animal de la que van a heredar dos diferentes clases, perro y avestruz, y una clase que lo contendrá Vet.

Vamos a empezar definiendo nuestra clase padre:

@Getter
@Setter
public abstract class Animal {
    private String type;
    private int legsNumber;

    protected Animal(String type, int legsNumber) {
        this.type = type;
        this.legsNumber = legsNumber;
    }
}

Ahora vamos a crear la clase perro (Dog)

@Getter
@Setter
public class Dog extends Animal {
    private String model;

    public Dog(String type, int legsNumber, String model) {
        super(type, legsNumber);
        this.model = model;
    }

}

Ahora crearemos la subclase avestruz (Ostrich).

@Getter
@Setter
public class Ostrich extends Animal {
    private int age;

    public Ostrich(String type, int legsNumber, int age) {
        super(type, legsNumber);
        this.age = age;
    }

}

Y finalmente crearemos la clase Veterinario (vet), donde incluímos una lista de animales:

@Getter
@Setter
public class Vet {

    private List<Animal> animals;
    
   
}

Para declarar la información una sola y única vez vamos a realizar una activación en un ObjectMapper, es la mejor opción si tenemos varios tipos involucrados. Esta activación se hará mediante el método activateDefaultTyping.

El método activateDefaultTyping acepta diferentes parámetros, siendo obligatorio en todos hacer uso de PolymorphicTypeValidator como parámetro de seguridad.

A continuación vamos a proceder a activar de manera global el mapper . Y a parte instanciaremos dos objectos, uno dog y otro Ostrich para añadirlos a la clase Vet. Todo esto lo haremos através de una clase de test:

  @Test
  public void shouldSerializeAnimals() {

    PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
        .builder()
        .allowIfSubType(Animal.class)
        .build();

    ObjectMapper mapper =
        JsonMapper.builder()
            .activateDefaultTyping(ptv, DefaultTyping.NON_CONCRETE_AND_ARRAYS)
            .build();

    Dog dog = new Dog("mammal", 4, "Huski");
    Ostrich ostrich = new Ostrich("oviparous ", 2, 5);

    List<Animal> animals = new ArrayList<>();
    animals.add(dog);
    animals.add(ostrich);

    Vet vet = new Vet();
    vet.setAnimals(animals);
    mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet);

    System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet));

  }

Output:
{
  "animals" : [ "java.util.ArrayList", [ [ "com.refactorizando.jackson.Dog", {
    "type" : "mammal",
    "legsNumber" : 4,
    "model" : "Huski"
  } ], [ "com.refactorizando.jackson.Ostrich", {
    "type" : "oviparous ",
    "legsNumber" : 2,
    "age" : 5
  } ] ] ]
}

Anotaciones en las Clases

Aunque lo anterior funciona y es fácil de entender, añade mucho boilerplate al código, por lo que en algunas ocasiones puede llegar a complicarlo más de lo necesario.

Así que Jackson al igual que con las anotaciones para serializar y deserializar clases nos facilita mucho la programación añadiendo anotaciones. Para la herencia, vamos añadir anotaciones en las clases.

Para poder conseguir mediante anotaciones la serialización y deserialización, vamos a hacer uso de @JsonTypeInfo para la clase padre y de @JsonSubTypes para las clases hijos, esta información deberá ser añadida en la clase padre, vamos a verlo con el ejemplo anterior.

@Getter
@Setter
@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME, 
  include = JsonTypeInfo.As.PROPERTY, 
  property = "type")
@JsonSubTypes({ 
  @Type(value = Dog.class, name = "dog"), 
  @Type(value = Ostrich.class, name = "ostrich") 
})
public abstract class Animal {
    private String type;
    private int legsNumber;

    protected Animal(String type, int legsNumber) {
        this.type = type;
        this.legsNumber = legsNumber;
    }
}

Como punto a destacar lo que hemos hecho ha sido añadir la propiedad «type», mapeada con los valores dog y ostrich. De esta manera cuando nuestro json salga por pantalla tendrá un nuevo campo.

Ejecutamos el siguiente test:

  @Test
  public void shouldSerializeAnimals()  throws JsonProcessingException {

    ObjectMapper mapper = new ObjectMapper();

    DogInclusionAnnotation dog = new DogInclusionAnnotation("mammal", 4, "Huski");
    OstrichInclusionAnnotation ostrich = new OstrichInclusionAnnotation("oviparous ", 2, 5);

    List<AnimalInclusionAnnotation> animals = new ArrayList<>();
    animals.add(dog);
    animals.add(ostrich);

    VetInclusionAnnotation vet = new VetInclusionAnnotation();
    vet.setAnimalInclusionAnnotations(animals);

    System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet));

  }
}

Obtenemos el siguiente output:

Output:
{
  "animalInclusionAnnotations" : [ {
    "animalType" : "dog",
    "type" : "mammal",
    "legsNumber" : 4,
    "model" : "Huski"
  }, {
    "animalType" : "ostrich",
    "type" : "oviparous ",
    "legsNumber" : 2,
    "age" : 5
  } ]
}

Ignorar propiedades en herencia

En muchas ocasiones vamos a tener que ignorar algunos campos de nuestras clases para que no sean mapeadas por los valores que entran o que salen. Para esos casos vamos a ver diferentes maneras de ignorarlas.

Ignorar propiedades en herencia por anotaciones

Por lo general el uso de anotaciones, suele ser lo más comodo y práctico, además nos ayudará a crear muchas líneas de código.

Vamos a partir de la clase anterior padre animal y de sus hijos:

@JsonIgnoreProperties({ "type", "model" })
@Getter
@Setter
public class DogIgnoreAnnotation extends AnimalIgnoreAnnotation{

  private String model;

  private String name;

  public DogIgnoreAnnotation(String type, int legsNumber, String model, String name) {
    super(type, legsNumber);
    this.model = model;
    this.name = name;
  }
}

Con las anotaciones anteriores, por un lado, ignoramos tanto la del padre como la del hijo haciendo uso de JsonIgnoreProperties. Y si queremos y ignorar una propiedad en concreto haremos uso de @JsonIgnore.

A continuación vamos a ver como lo ejecutamos:

  @Test
  public void shouldSerializeAnimals() throws JsonProcessingException {

    ObjectMapper mapper = new ObjectMapper();

    DogIgnoreAnnotation dog = new DogIgnoreAnnotation("mammal", 4, "Huski", "bruno");
    OstrichIgnoreAnnotation ostrich = new OstrichIgnoreAnnotation("oviparous ", 2, 5);

    List<AnimalIgnoreAnnotation> animals = new ArrayList<>();
    animals.add(dog);
    animals.add(ostrich);

    VetIgnoreAnnotation vet = new VetIgnoreAnnotation();
    vet.setAnimalIgnoreAnnotationList(animals);
    mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet);

    System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet));

  }
Output:
{
  "animalIgnoreAnnotationList" : [ {
    "legsNumber" : 4,
    "name" : "bruno"
  }, {
    "type" : "oviparous ",
    "legsNumber" : 2,
    "age" : 5
  } ]
}

Ignorar propiedades por introspección usando Jackson

Aunque ignorar propiedades en tu clase o clases por introspección usando Jackson no es de lo más común y tampoco conocido, es una manera muy potente para ignorar propiedades. Esta manera nos va a permitir hacer uso de hasIgnoreMarker para marcar las propiedades a evitar.

Para poder hacer uso la introspección de Jackson habrá que utilizar y extender la clase JacksonAnnotationIntrospector.

Para el siguiente ejemplo vamos a ignorar como hicimos anteriormente el type y el model de un perro:

class JacksonIntrospectorExample extends JacksonAnnotationIntrospector {
    public boolean hasIgnoreMarker(AnnotatedMember annotatedMember) {
        return mannotatedMember.getDeclaringClass() == Animal.class && annotatedMember.getName() == "type" 
          || annotatedMember.getDeclaringClass() == Dog.class 
          || annotatedMember.getName() == "legsNumber" 
          || super.hasIgnoreMarker(annotatedMember);
    }
}
  @Test
  public void shouldSerializeAnimals() throws JsonProcessingException {

    ObjectMapper mapper = new ObjectMapper();
    mapper.setAnnotationIntrospector(new JacksonIntrospector());

    DogIgnoreIntrospection dog = new DogIgnoreIntrospection("mammal", 4, "Huski");
    OstrichIgnoreIntrospection ostrich = new OstrichIgnoreIntrospection("oviparous ", 2, 5);

    List<AnimalIgnoreIntrospection> animals = new ArrayList<>();
    animals.add(dog);
    animals.add(ostrich);

    VetIgnoreIntrospection vet = new VetIgnoreIntrospection();
    vet.setAnimalIgnoreIntrospections(animals);
    mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet);

    System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vet));

  }
Output:
{
  "animalIgnoreIntrospections" : [ {
    "animalType" : "dog"
  }, {
    "animalType" : "ostrich",
    "age" : 5
  } ]
}

Conclusión

En este artículo sobre Serialización y Deserialización en Herencia con Jackson, hemos visto diversas maneras para agregar e ignorar campos en nuestras clases, haciendo uso de anotaciones y clases Java de una manera fácil y sencilla. Jackson es sin duda una librería de gran utilidad en nuestras aplicaciones.

Si quieres puedes ver el código de los ejemplos en nuestro Github.

No te olvides de seguirnos en nuestras redes sociales Facebook o Twitter para estar actualizado.

Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com y te ayudaremos encantados!


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *