Héritage avec Jackson

1. Vue d’ensemble

Dans cet article, nous verrons comment utiliser les hiérarchies de classes de Jackson.

Deux cas d’utilisation typiques sont l’inclusion de métadonnées de sous-type et les propriétés ignorées héritées des superclasses. Nous allons décrire ces deux scénarios et quelques circonstances dans lesquelles un traitement spécial des sous-types est nécessaire.

2. Inclusion d’informations de sous-type

Il existe deux manières d’ajouter des informations de type lors de la sérialisation et de la désérialisation des objets de données: la saisie globale par défaut et les annotations par classe.

2.1. Saisie globale par défaut

Les trois classes Java suivantes seront utilisées pour illustrer l’inclusion globale des métadonnées de type.

Super-classe de véhicule:

public abstract class Vehicle {
    private String make;
    private String model;

    protected Vehicle(String make, String model) {
        this.make = make;
        this.model = model;
    }

   //no-arg constructor, getters and setters
}

Car sous-classe:

public class Car extends Vehicle {
    private int seatingCapacity;
    private double topSpeed;

    public Car(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }

   //no-arg constructor, getters and setters
}

Truck sous-classe:

public class Truck extends Vehicle {
    private double payloadCapacity;

    public Truck(String make, String model, double payloadCapacity) {
        super(make, model);
        this.payloadCapacity = payloadCapacity;
    }

   //no-arg constructor, getters and setters
}

Le typage par défaut global permet de déclarer les informations de type une seule fois en les activant sur un objet ObjectMapper . Ces métadonnées de type seront ensuite appliquées à tous les types désignés. En conséquence, il est très pratique d’utiliser cette méthode pour ajouter des métadonnées de type, en particulier lorsque le nombre de types impliqués est important. L’inconvénient est qu’il utilise des noms de type Java pleinement qualifiés comme identificateurs de type et qu’il ne convient donc pas aux interactions avec des systèmes non Java. Il ne s’applique qu’à plusieurs types de types prédéfinis.

La structure Vehicle ci-dessus est utilisée pour renseigner une instance de la classe Fleet :

public class Fleet {
    private List<Vehicle> vehicles;

   //getters and setters
}

Pour incorporer des métadonnées de type, nous devons activer la fonctionnalité de saisie sur l’objet ObjectMapper qui sera utilisée ultérieurement pour la sérialisation et la désérialisation des objets de données:

ObjectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)

Le paramètre applicability détermine les types nécessitant des informations de type et le paramètre includeAs est le mécanisme d’inclusion de métadonnées de type. De plus, deux autres variantes de la méthode enableDefaultTyping sont fournies:

  • __ObjectMapper.enableDefaultTyping (ObjectMapper.DefaultTyping

applicabilité) : permet à l’appelant de spécifier l’applicabilité , tandis que en utilisant WRAPPER ARRAY comme valeur par défaut pour includeAs ** ObjectMapper.enableDefaultTyping (): utilise OBJECT AND NON CONCRETE

comme valeur par défaut pour applicability et WRAPPER ARRAY comme valeur par défaut pour includeAs__

Voyons voir comment ça fonctionne. Pour commencer, nous devons créer un objet ObjectMapper et activer la saisie par défaut dessus:

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();

L’étape suivante consiste à instancier et à renseigner la structure de données introduite au début de cette sous-section. Le code à faire sera réutilisé ultérieurement dans les sous-sections suivantes. Par souci de commodité et de réutilisation, nous l’appellerons le bloc d’instanciation de véhicule .

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = new Truck("Isuzu", "NQR", 7500.0);

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(car);
vehicles.add(truck);

Fleet serializedFleet = new Fleet();
serializedFleet.setVehicles(vehicles);

Ces objets peuplés seront alors sérialisés:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

La chaîne JSON résultante:

{
    "vehicles":
   [        "java.util.ArrayList",
       [           [                "org.baeldung.jackson.inheritance.Car",
                {
                    "make": "Mercedes-Benz",
                    "model": "S500",
                    "seatingCapacity": 5,
                    "topSpeed": 250.0
                }
           ],

           [                "org.baeldung.jackson.inheritance.Truck",
                {
                    "make": "Isuzu",
                    "model": "NQR",
                    "payloadCapacity": 7500.0
                }
           ]       ]   ]}

Lors de la désérialisation, les objets sont récupérés à partir de la chaîne JSON avec les données de type préservées:

Fleet deserializedFleet = mapper.readValue(jsonDataString, Fleet.class);

Les objets recréés seront les mêmes sous-types concrets qu’avant la sérialisation:

assertThat(deserializedFleet.getVehicles().get(0), instanceOf(Car.class));
assertThat(deserializedFleet.getVehicles().get(1), instanceOf(Truck.class));

2.2. Annotations par classe

L’annotation par classe est une méthode puissante pour inclure des informations de type et peut être très utile pour les cas d’utilisation complexes nécessitant un niveau de personnalisation important. Cependant, cela ne peut être réalisé qu’aux dépens de la complication. Les annotations par classe remplacent la saisie globale par défaut si les informations de type sont configurées de deux manières.

Pour utiliser cette méthode, le super-type doit être annoté avec @ JsonTypeInfo et plusieurs autres annotations pertinentes. Cette sous-section utilisera un modèle de données similaire à la structure Vehicle de l’exemple précédent pour illustrer les annotations par classe. Le seul changement est l’ajout d’annotations sur la classe abstraite Vehicle , comme indiqué ci-dessous:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME,
  include = JsonTypeInfo.As.PROPERTY,
  property = "type")
@JsonSubTypes({
  @Type(value = Car.class, name = "car"),
  @Type(value = Truck.class, name = "truck")
})
public abstract class Vehicle {
   //fields, constructors, getters and setters
}

Les objets de données sont créés à l’aide du bloc d’instanciation de véhicule introduit dans la sous-section précédente, puis sérialisés:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

La sérialisation produit la structure JSON suivante:

{
    "vehicles":
   [        {
            "type": "car",
            "make": "Mercedes-Benz",
            "model": "S500",
            "seatingCapacity": 5,
            "topSpeed": 250.0
        },

        {
            "type": "truck",
            "make": "Isuzu",
            "model": "NQR",
            "payloadCapacity": 7500.0
        }
   ]}

Cette chaîne est utilisée pour recréer des objets de données:

Fleet deserializedFleet = mapper.readValue(jsonDataString, Fleet.class);

Enfin, tout le progrès est validé:

assertThat(deserializedFleet.getVehicles().get(0), instanceOf(Car.class));
assertThat(deserializedFleet.getVehicles().get(1), instanceOf(Truck.class));

3. Ignorer les propriétés d’un supertype

Parfois, certaines propriétés héritées des superclasses doivent être ignorées lors de la sérialisation ou de la désérialisation. Cela peut être réalisé par l’une des trois méthodes suivantes: annotations, mix-in et introspection des annotations.

3.1. Annotations

Il existe deux annotations de Jackson couramment utilisées pour ignorer les propriétés, à savoir @ JsonIgnore et @ JsonIgnoreProperties . Le premier s’applique directement aux membres du type, indiquant à Jackson d’ignorer la propriété correspondante lors de la sérialisation ou de la désérialisation. Ce dernier est utilisé à n’importe quel niveau, y compris le type et le membre de type, pour répertorier les propriétés à ignorer.

@ JsonIgnoreProperties est plus puissant que l’autre car il nous permet d’ignorer les propriétés héritées des supertypes sur lesquels nous n’avons aucun contrôle, tels que les types d’une bibliothèque externe. De plus, cette annotation nous permet d’ignorer plusieurs propriétés à la fois, ce qui peut conduire à un code plus compréhensible dans certains cas.

La structure de classe suivante est utilisée pour illustrer l’utilisation des annotations:

public abstract class Vehicle {
    private String make;
    private String model;

    protected Vehicle(String make, String model) {
        this.make = make;
        this.model = model;
    }

   //no-arg constructor, getters and setters
}

@JsonIgnoreProperties({ "model", "seatingCapacity" })
public abstract class Car extends Vehicle {
    private int seatingCapacity;

    @JsonIgnore
    private double topSpeed;

    protected Car(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }

   //no-arg constructor, getters and setters
}

public class Sedan extends Car {
    public Sedan(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model, seatingCapacity, topSpeed);
    }

   //no-arg constructor
}

public class Crossover extends Car {
    private double towingCapacity;

    public Crossover(String make, String model, int seatingCapacity,
      double topSpeed, double towingCapacity) {
        super(make, model, seatingCapacity, topSpeed);
        this.towingCapacity = towingCapacity;
    }

   //no-arg constructor, getters and setters
}

Comme vous pouvez le constater, @ JsonIgnore dit à Jackson d’ignorer la propriété Car.topSpeed , tandis que @ JsonIgnoreProperties ignore les propriétés Vehicle.model et Car.seatingCapacity .

Le comportement des deux annotations est validé par le test suivant.

Premièrement, nous devons instancier ObjectMapper et les classes de données, puis utiliser cette instance ObjectMapper pour sérialiser des objets de données:

ObjectMapper mapper = new ObjectMapper();

Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0);
Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0);

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString contient le tableau JSON suivant:

----[    {
        "make": "Mercedes-Benz"
    },
    {
        "make": "BMW",
        "towingCapacity": 6000.0
    }]----

Enfin, nous prouverons la présence ou l’absence de divers noms de propriété dans la chaîne JSON résultante:

assertThat(jsonDataString, containsString("make"));
assertThat(jsonDataString, not(containsString("model")));
assertThat(jsonDataString, not(containsString("seatingCapacity")));
assertThat(jsonDataString, not(containsString("topSpeed")));
assertThat(jsonDataString, containsString("towingCapacity"));

3.2. Mix-ins

Les mixages nous permettent d’appliquer un comportement (par exemple, ignorer des propriétés lors de la sérialisation et de la désérialisation) sans qu’il soit nécessaire d’appliquer directement des annotations à une classe. Ceci est particulièrement utile lorsqu’il s’agit de classes tierces, dans lesquelles nous ne pouvons pas modifier le code directement.

Cette sous-section reprend la chaîne d’héritage de classe introduite dans la précédente, sauf que les annotations @ JsonIgnore et @ JsonIgnoreProperties de la classe Car ont été supprimées:

public abstract class Car extends Vehicle {
    private int seatingCapacity;
    private double topSpeed;

   //fields, constructors, getters and setters
}

Pour illustrer les opérations de mélange, nous allons ignorer les propriétés Vehicle.make et Car.topSpeed , puis utiliser un test pour nous assurer que tout fonctionne comme prévu.

La première étape consiste à déclarer un type de mix-in:

private abstract class CarMixIn {
    @JsonIgnore
    public String make;
    @JsonIgnore
    public String topSpeed;
}

Ensuite, le mix-in est lié à une classe de données via un objet ObjectMapper :

ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Car.class, CarMixIn.class);

Après cela, nous instancions des objets de données et les sérialisons dans une chaîne:

Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0);
Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0);

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString contient maintenant le JSON suivant:

----[    {
        "model": "S500",
        "seatingCapacity": 5
    },
    {
        "model": "X6",
        "seatingCapacity": 5,
        "towingCapacity": 6000.0
    }]----

Enfin, vérifions le résultat:

assertThat(jsonDataString, not(containsString("make")));
assertThat(jsonDataString, containsString("model"));
assertThat(jsonDataString, containsString("seatingCapacity"));
assertThat(jsonDataString, not(containsString("topSpeed")));
assertThat(jsonDataString, containsString("towingCapacity"));

3.3. Introspection d’annotation

L’introspection d’annotation est la méthode la plus puissante pour ignorer les propriétés de super-type, car elle permet une personnalisation détaillée à l’aide de l’API AnnotationIntrospector.hasIgnoreMarker .

Cette sous-section utilise la même hiérarchie de classes que la précédente. Dans ce cas d’utilisation, nous demanderons à Jackson d’ignorer Vehicle.model , Crossover.towingCapacity et toutes les propriétés déclarées dans la classe Car . Commençons par la déclaration d’une classe qui étend l’interface JacksonAnnotationIntrospector :

class IgnoranceIntrospector extends JacksonAnnotationIntrospector {
    public boolean hasIgnoreMarker(AnnotatedMember m) {
        return m.getDeclaringClass() == Vehicle.class && m.getName() == "model"
          || m.getDeclaringClass() == Car.class
          || m.getName() == "towingCapacity"
          || super.hasIgnoreMarker(m);
    }
}

L’introspecteur ignorera toutes les propriétés (c’est-à-dire qu’il les traitera comme si elles avaient été marquées comme ignorées via l’une des autres méthodes) qui correspondent à l’ensemble de conditions défini dans la méthode.

L’étape suivante consiste à enregistrer une instance de la classe IgnoranceIntrospector avec un objet ObjectMapper :

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

Maintenant, nous créons et sérialisons des objets de données de la même manière que dans la section 3.2. Le contenu de la nouvelle chaîne produite est:

----[    {
        "make": "Mercedes-Benz"
    },
    {
        "make": "BMW"
    }]----

Enfin, nous vérifierons que l’introspecteur a fonctionné comme prévu:

assertThat(jsonDataString, containsString("make"));
assertThat(jsonDataString, not(containsString("model")));
assertThat(jsonDataString, not(containsString("seatingCapacity")));
assertThat(jsonDataString, not(containsString("topSpeed")));
assertThat(jsonDataString, not(containsString("towingCapacity")));

4. Scénarios de gestion de sous-types

Cette section traitera de deux scénarios intéressants relatifs à la gestion des sous-classes.

4.1. Conversion entre sous-types

Jackson permet de convertir un objet en un type différent de celui d’origine. En fait, cette conversion peut se produire parmi tous les types compatibles, mais elle est particulièrement utile lorsqu’elle est utilisée entre deux sous-types de la même interface ou classe pour sécuriser les valeurs et les fonctionnalités.

Afin de démontrer la conversion d’un type en un autre, nous allons réutiliser la hiérarchie Vehicle tirée de la section 2, avec l’ajout de l’annotation @ JsonIgnore sur les propriétés de Car et Truck pour éviter les incompatibilités.

public class Car extends Vehicle {
    @JsonIgnore
    private int seatingCapacity;

    @JsonIgnore
    private double topSpeed;

   //constructors, getters and setters
}

public class Truck extends Vehicle {
    @JsonIgnore
    private double payloadCapacity;

   //constructors, getters and setters
}

Le code suivant vérifiera qu’une conversion a réussi et que le nouvel objet conserve les valeurs de données de l’ancien:

ObjectMapper mapper = new ObjectMapper();

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = mapper.convertValue(car, Truck.class);

assertEquals("Mercedes-Benz", truck.getMake());
assertEquals("S500", truck.getModel());

4.2. Désérialisation sans constructeurs no-arg

Par défaut, Jackson recrée les objets de données à l’aide de constructeurs no-arg.

Cela peut être gênant dans certains cas, par exemple quand une classe a des constructeurs autres que ceux par défaut et que les utilisateurs doivent écrire des non-arguments juste pour satisfaire les exigences de Jackson. C’est encore plus gênant dans une hiérarchie de classes où un constructeur sans argument doit être ajouté à une classe et tous ceux situés plus haut dans la chaîne d’héritage. Dans ces cas, les méthodes creator viennent à la rescousse.

Cette section utilisera une structure d’objet similaire à celle de la section 2, avec quelques modifications apportées aux constructeurs. Plus précisément, tous les constructeurs sans argument sont supprimés et les constructeurs de sous-types concrets sont annotés avec @ JsonCreator et @ JsonProperty pour en faire des méthodes créateur.

public class Car extends Vehicle {

    @JsonCreator
    public Car(
      @JsonProperty("make") String make,
      @JsonProperty("model") String model,
      @JsonProperty("seating") int seatingCapacity,
      @JsonProperty("topSpeed") double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
            this.topSpeed = topSpeed;
    }

   //fields, getters and setters
}

public class Truck extends Vehicle {

    @JsonCreator
    public Truck(
      @JsonProperty("make") String make,
      @JsonProperty("model") String model,
      @JsonProperty("payload") double payloadCapacity) {
        super(make, model);
        this.payloadCapacity = payloadCapacity;
    }

   //fields, getters and setters
}

Un test vérifiera que Jackson peut gérer des objets dépourvus de constructeur sans argument:

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = new Truck("Isuzu", "NQR", 7500.0);

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(car);
vehicles.add(truck);

Fleet serializedFleet = new Fleet();
serializedFleet.setVehicles(vehicles);

String jsonDataString = mapper.writeValueAsString(serializedFleet);
mapper.readValue(jsonDataString, Fleet.class);

5. Conclusion

Ce didacticiel a couvert plusieurs cas d’utilisation intéressants afin de démontrer la prise en charge de Jackson pour l’héritage de types, en mettant l’accent sur le polymorphisme et la méconnaissance des propriétés de supertype.

Vous trouverez la mise en œuvre de tous ces exemples et extraits de code dans a projet GitHub .