JSON: deserializzare una lista di oggetti di sottoclassi di una classe astratta con Jackson

In questo post vediamo come effettuare la serializzazione e, soprattutto, la deserializzazione JSON di una classe Java che dichiara una variabile di istanza costituita da una lista di oggetti di una classe astratta, all’interno della quale sono presenti oggetti delle sue diverse sottoclassi concrete.

Iniziamo con il creare la nostra classe astratta:

public abstract class MyItem {

    private int id;
    private String name;

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public MyItem(int id, String name) {
        this.id = id;
        this.name = name;
    }

}

Proseguiamo creando 3 diverse sottoclassi concrete della classe astratta MyItem:

public class MySubItemA extends MyItem {

    private String itemAProperty1;
    private String itemAProperty2;

    public String getItemAProperty1() {
        return this.itemAProperty1;
    }

    public void setItemAProperty1(String itemAProperty1) {
        this.itemAProperty1 = itemAProperty1;
    }

    public String getItemAProperty2() {
        return this.itemAProperty2;
    }

    public void setItemAProperty2(String itemAProperty2) {
        this.itemAProperty2 = itemAProperty2;
    }


    public MySubItemA(int id, String name, String p1, String p2) {
        super(id, name);
        this.itemAProperty1 = p1;
        this.itemAProperty2 = p2;
    }
}
public class MySubItemB extends MyItem {

    private int itemBProperty1;
    private String itemBProperty2;

    public int getItemBProperty1() {
        return this.itemBProperty1;
    }

    public void setItemBProperty1(int itemBProperty1) {
        this.itemBProperty1 = itemBProperty1;
    }

    public String getItemBProperty2() {
        return this.itemBProperty2;
    }

    public void setItemBProperty2(String itemBProperty2) {
        this.itemBProperty2 = itemBProperty2;
    }

    public MySubItemB(int id, String name, int p1, String p2) {
        super(id, name);
        this.itemBProperty1 = p1;
        this.itemBProperty2 = p2;
    }
}
public class MySubItemC extends MyItem {

    private int itemCProperty1;
    private int itemCProperty2;

    public int getItemCProperty1() {
        return this.itemCProperty1;
    }

    public void setItemCProperty1(int itemCProperty1) {
        this.itemCProperty1 = itemCProperty1;
    }

    public int getItemCProperty2() {
        return this.itemCProperty2;
    }

    public void setItemCProperty2(int itemCProperty2) {
        this.itemCProperty2 = itemCProperty2;
    }

    public MySubItemC(int id, String name, int p1, int p2) {
        super(id, name);
        this.itemCProperty1 = p1;
        this.itemCProperty2 = p2;
    }
}

Creiamo infine una classe client che conterrà una lista di oggetti della classe astratta

import java.util.ArrayList;
import java.util.List;

public class ClientObject {

    private List l;

    public ClientObject(List pl) {
        this.l = pl;
    }

    public ClientObject() {
        this.l = new ArrayList();
    }

    public List getL() {
        return this.l;
    }

    public void setL(List l) {
        this.l = l;
    }
}

A questo punto creiamo una classe di test in cui istanziamo alcuni oggetti delle sottoclassi concrete, li aggiungiamo alla lista di MyItem che rappresenta il membro di istanza della classe ClientObject e proviamo a serializzare l’oggetto ClientObject.

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class TestJSONSubclassing {

    public static void main(String[] args) {
        ObjectMapper mapper = new ObjectMapper();

        ClientObject c = new ClientObject();

        MyItem i1 = new MySubItemA(1, "Value1", "Some stuff", "Another property value");
        MyItem i2 = new MySubItemB(2, "Value2", 1000, "B property");
        MyItem i3 = new MySubItemC(3, "Value3", 2000, -1);
        MyItem i4 = new MySubItemA(4, "Value4", "Bla Bla Bla", "item A property");

        c.getL().add(i1);
        c.getL().add(i2);
        c.getL().add(i3);
        c.getL().add(i4);

        String s = null;
        try {
            s = mapper.writeValueAsString(c);
        }
        catch (JsonProcessingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(s);

    }
}

Provando ad eseguire il programma otteniamo il seguente output:

{"l":[{"id":1,"name":"Value1","itemAProperty1":"Some stuff","itemAProperty2":"Another property value"},{"id":2,"name":"Value2","itemBProperty1":1000,"itemBProperty2":"B property"},{"id":3,"name":"Value3","itemCProperty1":2000,"itemCProperty2":-1},{"id":4,"name":"Value4","itemAProperty1":"Bla Bla Bla","itemAProperty2":"item A property"}]}

Indichiamo al nostro oggetto ObjectMapper di Jackson di utilizzare l’opzione di formattazione indentata della stringa JSON in modo da renderla più leggibile:

mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
{
  "l" : [ {
    "id" : 1,
    "name" : "Value1",
    "itemAProperty1" : "Some stuff",
    "itemAProperty2" : "Another property value"
  }, {
    "id" : 2,
    "name" : "Value2",
    "itemBProperty1" : 1000,
    "itemBProperty2" : "B property"
  }, {
    "id" : 3,
    "name" : "Value3",
    "itemCProperty1" : 2000,
    "itemCProperty2" : -1
  }, {
    "id" : 4,
    "name" : "Value4",
    "itemAProperty1" : "Bla Bla Bla",
    "itemAProperty2" : "item A property"
  } ]
}

Come possiamo vedere gli elementi della lista di oggetti MyItem sono stati serializzati correttamente come stringa JSON, con tutte le proprietà valorizzate. Quello che manca però è l’indicazione, per ciascun oggetto, di quale sia il suo tipo reale, cioè di quale delle sottoclassi concrete sia una istanza. Senza questa informazione, la deserializzazione della rappresentazione JSON in una lista di oggetti delle sottoclassi concrete non è possibile,
in quanto non si può conoscere il tipo esatto degli oggetti.

Proviamo a vedere cosa succede effettuando la deserializzazione dell’oggetto ClientObject. Modifichiamo il codice della nostra classe di test come segue, inserendo l’istruzione per la deserializzazione in un nuovo oggetto ClientObject ed un ciclo per la stampa degli oggetti contenuti nella sua lista di MyItem:

import java.io.IOException;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

public class TestJSONSubclassing {

    public static void main(String[] args) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.INDENT_OUTPUT, true);

        ClientObject c = new ClientObject();

        MyItem i1 = new MySubItemA(1, "Value1", "Some stuff", "Another property value");
        MyItem i2 = new MySubItemB(2, "Value2", 1000, "B property");
        MyItem i3 = new MySubItemC(3, "Value3", 2000, -1);
        MyItem i4 = new MySubItemA(4, "Value4", "Bla Bla Bla", "item A property");

        c.getL().add(i1);
        c.getL().add(i2);
        c.getL().add(i3);
        c.getL().add(i4);

        String s = null;
        try {
            s = mapper.writeValueAsString(c);
        }
        catch (JsonProcessingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // System.out.println(s);

        ClientObject c2 = null;
        try {
            c2 = mapper.readValue(s, ClientObject.class);
        }
        catch (JsonParseException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (JsonMappingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        if (c2 != null) {
            System.out.println("----- Items List -----");

            for (MyItem mi : c2.getL())
                System.out.println("Type = " + mi.getClass() +  ", id = "+ mi.getId() + ", name = " + mi.getName());
        }
    }
}

Eseguendo il programma otteniamo il seguente risultato:

com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of net.davismol.jsonsubclassing.MyItem, problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information
 at [Source: {
  "l" : [ {
    "id" : 1,
    "name" : "Value1",
    "itemAProperty1" : "Some stuff",
    "itemAProperty2" : "Another property value"
  }, {
    "id" : 2,
    "name" : "Value2",
    "itemBProperty1" : 1000,
    "itemBProperty2" : "B property"
  }, {
    "id" : 3,
    "name" : "Value3",
    "itemCProperty1" : 2000,
    "itemCProperty2" : -1
  }, {
    "id" : 4,
    "name" : "Value4",
    "itemAProperty1" : "Bla Bla Bla",
    "itemAProperty2" : "item A property"
  } ]
}; line: 2, column: 11] (through reference chain: net.davismol.jsonsubclassing.ClientObject["l"]->java.util.ArrayList[0])
	at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:148)
	at com.fasterxml.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:771)
	at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:140)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:232)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:206)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:25)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:538)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:99)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:238)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:118)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3051)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2146)
	at net.davismol.jsonsubclassing.TestJSONSubclassing.main(TestJSONSubclassing.java:42)

Come possiamo vedere, e come avevamo previsto, viene generata un’eccezione che ci segnala che non si possono istanziare oggetti di una classe astratta e che quindi dobbiamo mappare gli oggetti con delle classi concrete oppure creare un deserializzatore dedicato.

La soluzione che adottiamo per risolvere il problema è la prima, cioè definiamo nella classe MyItem quali sono le sottoclassi concrete che ereditano da essa ed aggiungiamo questa informazione nel JSON generato dalla serializzazione, in modo che poi, in fase di deserializzazione sia possibile istanziare un oggetto del tipo concreto corretto.

Per fare questo utilizziamo alcune annotazioni messe a disposizione da Jackson: @JsonTypeInfo, @JsonSubTypes e @Type

  • @JsonTypeInfo: viene utilizzata per indicare di aggiungere nella serializzazione una nuova property relativa al tipo dell’oggetto e di definire il suo nome
  • @JsonSubTypes: permette di definire quali sono le sottoclassi della classe su cui è applicata l’annotation
  • @Type: indica le sottoclassi vere e proprie e permette, eventualmente di assegnargli un “nome”

Vediamo un primo esempio, modificando il codice della classe MyItem come segue:

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;

@JsonTypeInfo(use = Id.CLASS,
              include = JsonTypeInfo.As.PROPERTY,
              property = "type")
@JsonSubTypes({
    @Type(value = MySubItemA.class),
    @Type(value = MySubItemB.class),
    @Type(value = MySubItemC.class),
    })
public abstract class MyItem {

    private int id;
    private String name;

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public MyItem(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

Il risultato ottenuto in questo caso è quello che segue, dove possiamo vedere che nel JSON generato, per ogni elemento della lista di MyItem, viene aggiunta una nuova property chiamata “type” (definita dall’atributo dell’annotation property = “type”) e valorizzata con il nome completo della sottoclasse concreta di cui l’oggetto era un’istanza (specificata dall’attributo dell’annotation use = Id.CLASS). In questo modo, in fase di deserializzazione abbiamo l’informazione relativa alla classe concreta da utilizzare per istanziare ogni elemento.

{
  "l" : [ {
    "type" : "net.davismol.jsonsubclassing.MySubItemA",
    "id" : 1,
    "name" : "Value1",
    "itemAProperty1" : "Some stuff",
    "itemAProperty2" : "Another property value"
  }, {
    "type" : "net.davismol.jsonsubclassing.MySubItemB",
    "id" : 2,
    "name" : "Value2",
    "itemBProperty1" : 1000,
    "itemBProperty2" : "B property"
  }, {
    "type" : "net.davismol.jsonsubclassing.MySubItemC",
    "id" : 3,
    "name" : "Value3",
    "itemCProperty1" : 2000,
    "itemCProperty2" : -1
  }, {
    "type" : "net.davismol.jsonsubclassing.MySubItemA",
    "id" : 4,
    "name" : "Value4",
    "itemAProperty1" : "Bla Bla Bla",
    "itemAProperty2" : "item A property"
  } ]
}

Se modifichiamo l’attributo “use” dell’annotation da Id.CLASS ad Id.NAME (Id è un enumerator) otteniamo la proprietà “type” valorizzata con solo il nome della classe, invece del suo nome completo di package:

@JsonTypeInfo(use = Id.NAME,
              include = JsonTypeInfo.As.PROPERTY,
              property = "type")
{
  "l" : [ {
    "type" : "MySubItemA",
    "id" : 1,
    "name" : "Value1",
    "itemAProperty1" : "Some stuff",
    "itemAProperty2" : "Another property value"
  }, {
    "type" : "MySubItemB",
    "id" : 2,
    "name" : "Value2",
    "itemBProperty1" : 1000,
    "itemBProperty2" : "B property"
  }, {
    "type" : "MySubItemC",
    "id" : 3,
    "name" : "Value3",
    "itemCProperty1" : 2000,
    "itemCProperty2" : -1
  }, {
    "type" : "MySubItemA",
    "id" : 4,
    "name" : "Value4",
    "itemAProperty1" : "Bla Bla Bla",
    "itemAProperty2" : "item A property"
  } ]
}

Volendo è anche possibile ridefinire i “nomi” delle sottoclassi concrete da utilizzare come valori della proprietà “type” nella stringa JSON, utilizzando l’attributo “name” dell’annotazione @Type. Vediamo un esempio, modificando le annotations della classe MyItem come segue:

@JsonTypeInfo(use = Id.NAME,
              include = JsonTypeInfo.As.PROPERTY,
              property = "type")
@JsonSubTypes({
    @Type(value = MySubItemA.class, name = "subA"),
    @Type(value = MySubItemB.class, name = "subB"),
    @Type(value = MySubItemC.class, name = "subC"),
    })

In questo caso otteniamo come risultato della serializzazione:

{
  "l" : [ {
    "type" : "subA",
    "id" : 1,
    "name" : "Value1",
    "itemAProperty1" : "Some stuff",
    "itemAProperty2" : "Another property value"
  }, {
    "type" : "subB",
    "id" : 2,
    "name" : "Value2",
    "itemBProperty1" : 1000,
    "itemBProperty2" : "B property"
  }, {
    "type" : "subC",
    "id" : 3,
    "name" : "Value3",
    "itemCProperty1" : 2000,
    "itemCProperty2" : -1
  }, {
    "type" : "subA",
    "id" : 4,
    "name" : "Value4",
    "itemAProperty1" : "Bla Bla Bla",
    "itemAProperty2" : "item A property"
  } ]
}

Ora che abbiamo visto come includere nella stringa di serializzazione JSON l’informazione sul tipo reale degli oggetti, vediamo se la deserializzazione funziona correttamente. Prima però dobbiamo fare ancora una modifica alle nostre sottoclassi concrete, indicando qual è il costruttore da utilizzare, tramite l’annotation @JsonCreator, e come mappare le proprietà JSON con i suoi paramentri di input, tramite l’annotation @JsonProperty.

La classe MySubItemA deve quindi essere modificata come segue ed, allo stesso modo, anche le classi MySubItemB e MySubItemC.

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class MySubItemA extends MyItem {

    private String itemAProperty1;
    private String itemAProperty2;

    public String getItemAProperty1() {
        return this.itemAProperty1;
    }

    public void setItemAProperty1(String itemAProperty1) {
        this.itemAProperty1 = itemAProperty1;
    }

    public String getItemAProperty2() {
        return this.itemAProperty2;
    }

    public void setItemAProperty2(String itemAProperty2) {
        this.itemAProperty2 = itemAProperty2;
    }

    @JsonCreator
    public MySubItemA(@JsonProperty("id")int id, @JsonProperty("name")String name, @JsonProperty("itemAProperty1")String p1, @JsonProperty("itemAProperty2")String p2) {
        super(id, name);
        this.itemAProperty1 = p1;
        this.itemAProperty2 = p2;
    }

}

Ripristiniamo la nostra classe di test affinché esegua sia la serializzazine che la deserializzazione dell’oggetto ClientObject.

import java.io.IOException;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

public class TestJSONSubclassing {

    public static void main(String[] args) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.INDENT_OUTPUT, true);

        ClientObject c = new ClientObject();

        MyItem i1 = new MySubItemA(1, "Value1", "Some stuff", "Another property value");
        MyItem i2 = new MySubItemB(2, "Value2", 1000, "B property");
        MyItem i3 = new MySubItemC(3, "Value3", 2000, -1);
        MyItem i4 = new MySubItemA(4, "Value4", "Bla Bla Bla", "item A property");

        c.getL().add(i1);
        c.getL().add(i2);
        c.getL().add(i3);
        c.getL().add(i4);

        String s = null;
        try {
            s = mapper.writeValueAsString(c);
        }
        catch (JsonProcessingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        System.out.println(s);

        ClientObject c2 = null;
        try {
            c2 = mapper.readValue(s, ClientObject.class);
        }
        catch (JsonParseException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (JsonMappingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        if (c2 != null) {
            System.out.println("----- Items List -----");

            for (MyItem mi : c2.getL()) {
                System.out.println("Type = " + mi.getClass() +  ", id = "+ mi.getId() + ", name = " + mi.getName());
            }
        }
    }
}

Eseguendo nuovamente il programma otteniamo il risultato seguente:

{
  "l" : [ {
    "type" : "subA",
    "id" : 1,
    "name" : "Value1",
    "itemAProperty1" : "Some stuff",
    "itemAProperty2" : "Another property value"
  }, {
    "type" : "subB",
    "id" : 2,
    "name" : "Value2",
    "itemBProperty1" : 1000,
    "itemBProperty2" : "B property"
  }, {
    "type" : "subC",
    "id" : 3,
    "name" : "Value3",
    "itemCProperty1" : 2000,
    "itemCProperty2" : -1
  }, {
    "type" : "subA",
    "id" : 4,
    "name" : "Value4",
    "itemAProperty1" : "Bla Bla Bla",
    "itemAProperty2" : "item A property"
  } ]
}
----- Items List -----
Type = class net.davismol.jsonsubclassing.MySubItemA, id = 1, name = Value1
Type = class net.davismol.jsonsubclassing.MySubItemB, id = 2, name = Value2
Type = class net.davismol.jsonsubclassing.MySubItemC, id = 3, name = Value3
Type = class net.davismol.jsonsubclassing.MySubItemA, id = 4, name = Value4

Come possiamo vedere, questa volta la deserializzazione è andata a buon fine e la lista di oggetti MyItem è stata creata istanziando i vari oggetti delle corrette sottoclassi concrete.

Tutte le classi utilizzate nell’esempio di questo post sono scaricabili qui:

Leave a Reply

Your email address will not be published. Required fields are marked *