Eredità con Jackson

1. Panoramica

In questo articolo, daremo uno sguardo al lavoro con le gerarchie di classe a Jackson.

Due casi d'uso tipici sono l'inclusione di metadati del sottotipo e l'ignoranza delle proprietà ereditate dalle superclassi. Descriveremo questi due scenari e un paio di circostanze in cui è necessario un trattamento speciale dei sottotipi.

2. Inclusione di informazioni sui sottotipi

Esistono due modi per aggiungere informazioni sul tipo durante la serializzazione e la deserializzazione di oggetti dati, ovvero la tipizzazione predefinita globale e le annotazioni per classe.

2.1. Digitazione predefinita globale

Le seguenti tre classi Java verranno utilizzate per illustrare l'inclusione globale dei metadati di tipo.

Superclasse di veicoli :

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 }

Sottoclasse auto :

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 }

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

La digitazione predefinita globale consente di dichiarare le informazioni sul tipo solo una volta abilitandole su un oggetto ObjectMapper . I metadati di quel tipo verranno quindi applicati a tutti i tipi designati. Di conseguenza, è molto conveniente utilizzare questo metodo per aggiungere metadati di tipo, soprattutto quando sono coinvolti molti tipi. Lo svantaggio è che utilizza nomi di tipo Java completamente qualificati come identificatori di tipo, quindi non è adatto per interazioni con sistemi non Java ed è applicabile solo a diversi tipi di tipi predefiniti.

La struttura Veicolo mostrata sopra viene utilizzata per popolare un'istanza della classe Fleet :

public class Fleet { private List vehicles; // getters and setters }

Per incorporare i metadati del tipo, è necessario abilitare la funzionalità di digitazione sull'oggetto ObjectMapper che verrà utilizzato per la serializzazione e la deserializzazione degli oggetti dati in seguito:

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

Il parametro di applicabilità determina i tipi che richiedono informazioni sul tipo e il parametro includeAs è il meccanismo per l'inclusione dei metadati di tipo. Inoltre, vengono fornite altre due varianti del metodo enableDefaultTyping :

  • ObjectMapper.enableDefaultTyping (applicabilità ObjectMapper.DefaultTyping) : consente al chiamante di specificare l' applicabilità , mentre utilizza WRAPPER_ARRAY come valore predefinito per includeAs
  • ObjectMapper.enableDefaultTyping (): utilizza OBJECT_AND_NON_CONCRETE come valore predefinito per l' applicabilità e WRAPPER_ARRAY come valore predefinito per includeAs

Vediamo come funziona. Per iniziare, dobbiamo creare un oggetto ObjectMapper e abilitare la digitazione predefinita su di esso:

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

Il passaggio successivo consiste nell'istanziare e popolare la struttura dati introdotta all'inizio di questa sottosezione. Il codice per farlo verrà riutilizzato in seguito nelle sottosezioni successive. Per comodità e riutilizzo, lo chiameremo blocco di istanza del veicolo .

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);

Tali oggetti popolati verranno quindi serializzati:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

La stringa JSON risultante:

{ "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 } ] ] ] }

Durante la deserializzazione, gli oggetti vengono recuperati dalla stringa JSON con i dati di tipo conservati:

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

Gli oggetti ricreati saranno gli stessi sottotipi concreti di prima della serializzazione:

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

2.2. Annotazioni per classe

L'annotazione per classe è un metodo potente per includere informazioni sul tipo e può essere molto utile per casi d'uso complessi in cui è necessario un livello significativo di personalizzazione. Tuttavia, questo può essere ottenuto solo a scapito delle complicazioni. Le annotazioni per classe sovrascrivono la tipizzazione predefinita globale se le informazioni sul tipo sono configurate in entrambi i modi.

Per utilizzare questo metodo, il supertipo dovrebbe essere annotato con @JsonTypeInfo e molte altre annotazioni rilevanti. Questa sottosezione utilizzerà un modello di dati simile alla struttura del veicolo nell'esempio precedente per illustrare le annotazioni per classe. L'unico cambiamento è l'aggiunta di annotazioni sulla classe astratta del veicolo , come mostrato di seguito:

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

Gli oggetti dati vengono creati utilizzando il blocco di istanziazione del veicolo introdotto nella sottosezione precedente e quindi serializzati:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

La serializzazione produce la seguente struttura JSON:

{ "vehicles": [ { "type": "car", "make": "Mercedes-Benz", "model": "S500", "seatingCapacity": 5, "topSpeed": 250.0 }, { "type": "truck", "make": "Isuzu", "model": "NQR", "payloadCapacity": 7500.0 } ] }

Quella stringa viene utilizzata per ricreare gli oggetti dati:

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

Infine, l'intero progresso è convalidato:

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

3. Ignorare le proprietà di un supertipo

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 verificherà che Jackson possa gestire oggetti privi di costruttori no-arg:

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. conclusione

Questo tutorial ha coperto diversi casi d'uso interessanti per dimostrare il supporto di Jackson per l'ereditarietà dei tipi, con particolare attenzione al polimorfismo e all'ignoranza delle proprietà del supertipo.

L'implementazione di tutti questi esempi e frammenti di codice può essere trovata in un progetto GitHub.