Héritage avec Jackson

1. Vue d'ensemble

Dans cet article, nous verrons comment travailler avec les hiérarchies de classes dans Jackson.

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

2. Inclusion d'informations sur les sous-types

Il existe deux façons d'ajouter des informations de type lors de la sérialisation et de la désérialisation des objets de données, à savoir le typage global par défaut et les annotations par classe.

2.1. Saisie par défaut globale

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

Superclasse 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 }

Sous- classe de voiture :

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 }

Sous- classe de camion :

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 global par défaut permet aux informations de type d'être déclarées une seule fois en l'activant sur un objet ObjectMapper . Ces métadonnées de type seront ensuite appliquées à tous les types désignés. Par conséquent, il est très pratique d'utiliser cette méthode pour ajouter des métadonnées de type, en particulier lorsqu'il y a un grand nombre de types impliqués. L'inconvénient est qu'il utilise des noms de type Java pleinement qualifiés comme identificateurs de type, et n'est donc pas adapté aux interactions avec des systèmes non Java, et n'est applicable qu'à plusieurs types de types prédéfinis.

La structure Véhicule présentée ci-dessus est utilisée pour remplir une instance de la classe Fleet :

public class Fleet { private List 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é pour la sérialisation et la désérialisation des objets de données plus tard:

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

Le paramètre d' applicabilité 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. En outre, deux autres variantes de la méthode enableDefaultTyping sont fournies:

  • ObjectMapper.enableDefaultTyping (ObjectMapper.DefaultTyping applicability) : permet à l'appelant de spécifier l' applicabilité , tout en utilisant WRAPPER_ARRAY comme valeur par défaut pour includeAs
  • ObjectMapper.enableDefaultTyping (): utilise OBJECT_AND_NON_CONCRETE comme valeur par défaut pour l' applicabilité 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 à peupler la structure de données introduite au début de cette sous-section. Le code pour le faire sera réutilisé plus tard dans les sous-sections suivantes. Par souci de commodité et de réutilisation, nous l'appellerons le bloc d'instanciation du véhicule .

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0); Truck truck = new Truck("Isuzu", "NQR", 7500.0); List vehicles = new ArrayList(); vehicles.add(car); vehicles.add(truck); Fleet serializedFleet = new Fleet(); serializedFleet.setVehicles(vehicles);

Ces objets peuplés seront ensuite 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 où un niveau significatif de personnalisation est nécessaire. Cependant, cela ne peut être réalisé qu'au prix de complications. Les annotations par classe remplacent le typage global par défaut si les informations de type sont configurées dans les deux sens.

Pour utiliser cette méthode, le supertype doit être annoté avec @JsonTypeInfo et plusieurs autres annotations pertinentes. Cette sous-section utilisera un modèle de données similaire à la structure Véhicule de l'exemple précédent pour illustrer les annotations par classe. Le seul changement est l'ajout d'annotations sur la classe abstraite de véhicule , 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, toute la progression est validée:

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

Sometimes, some properties inherited from superclasses need to be ignored during serialization or deserialization. This can be achieved by one of three methods: annotations, mix-ins and annotation introspection.

3.1. Annotations

There are two commonly used Jackson annotations to ignore properties, which are @JsonIgnore and @JsonIgnoreProperties. The former is directly applied to type members, telling Jackson to ignore the corresponding property when serializing or deserializing. The latter is used at any level, including type and type member, to list properties that should be ignored.

@JsonIgnoreProperties is more powerful than the other since it allows us to ignore properties inherited from supertypes that we do not have control of, such as types in an external library. In addition, this annotation allows us to ignore many properties at once, which can lead to more understandable code in some cases.

The following class structure is used to demonstrate annotation usage:

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 }

As you can see, @JsonIgnore tells Jackson to ignore Car.topSpeed property, while @JsonIgnoreProperties ignores the Vehicle.model and Car.seatingCapacity ones.

The behavior of both annotations is validated by the following test. First, we need to instantiate ObjectMapper and data classes, then use that ObjectMapper instance to serialize data objects:

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 vehicles = new ArrayList(); vehicles.add(sedan); vehicles.add(crossover); String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString contains the following JSON array:

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

Finally, we will prove the presence or absence of various property names in the resulting JSON string:

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

Mix-ins allow us to apply behavior (such as ignoring properties when serializing and deserializing) without the need to directly apply annotations to a class. This is especially useful when dealing with third-party classes, in which we cannot modify the code directly.

This sub-section reuses the class inheritance chain introduced in the previous one, except that the @JsonIgnore and @JsonIgnoreProperties annotations on the Car class have been removed:

public abstract class Car extends Vehicle { private int seatingCapacity; private double topSpeed; // fields, constructors, getters and setters }

In order to demonstrate operations of mix-ins, we will ignore Vehicle.make and Car.topSpeed properties, then use a test to make sure everything works as expected.

The first step is to declare a mix-in type:

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

Next, the mix-in is bound to a data class through an ObjectMapper object:

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

After that, we instantiate data objects and serialize them into a string:

Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0); Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0); List vehicles = new ArrayList(); vehicles.add(sedan); vehicles.add(crossover); String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString now contains the following JSON:

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

Finally, let's verify the result:

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

3.3. Annotation Introspection

Annotation introspection is the most powerful method to ignore supertype properties since it allows for detailed customization using the AnnotationIntrospector.hasIgnoreMarker API.

This sub-section makes use of the same class hierarchy as the preceding one. In this use case, we will ask Jackson to ignore Vehicle.model, Crossover.towingCapacity and all properties declared in the Car class. Let's start with the declaration of a class that extends the JacksonAnnotationIntrospector interface:

class IgnoranceIntrospector extends JacksonAnnotationIntrospector { public boolean hasIgnoreMarker(AnnotatedMember m)  }

The introspector will ignore any properties (that is, it will treat them as if they were marked as ignored via one of the other methods) that match the set of conditions defined in the method.

The next step is to register an instance of the IgnoranceIntrospector class with an ObjectMapper object:

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

Now we create and serialize data objects in the same way as in section 3.2. The contents of the newly produced string are:

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

Finally, we'll verify that the introspector worked as intended:

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. Subtype Handling Scenarios

This section will deal with two interesting scenarios relevant to subclass handling.

4.1. Conversion Between Subtypes

Jackson allows an object to be converted to a type other than the original one. In fact, this conversion may happen among any compatible types, but it is most helpful when used between two subtypes of the same interface or class to secure values and functionality.

In order to demonstrate conversion of a type to another one, we will reuse the Vehicle hierarchy taken from section 2, with the addition of the @JsonIgnore annotation on properties in Car and Truck to avoid incompatibility.

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 }

The following code will verify that a conversion is successful and that the new object preserves data values from the old one:

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. Deserialization Without No-arg Constructors

By default, Jackson recreates data objects by using no-arg constructors. This is inconvenient in some cases, such as when a class has non-default constructors and users have to write no-arg ones just to satisfy Jackson's requirements. It is even more troublesome in a class hierarchy where a no-arg constructor must be added to a class and all those higher in the inheritance chain. In these cases, creator methods come to the rescue.

This section will use an object structure similar to the one in section 2, with some changes to constructors. Specifically, all no-arg constructors are dropped, and constructors of concrete subtypes are annotated with @JsonCreator and @JsonProperty to make them creator methods.

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 traiter des objets qui n'ont pas de constructeurs 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 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 pour démontrer le support de Jackson pour l'héritage de type, en mettant l'accent sur le polymorphisme et l'ignorance des propriétés des supertypes.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans un projet GitHub.