Serializzazione e deserializzazione JSON di oggetti Java con Jackson: un esempio concreto

[This article is available also in ENGLISH: JSON serialization and deserialization of JAVA objects with Jackson: a concrete example]

In questo post vediamo come utilizzare il framework Jackson per serializzare dei semplici oggetti Java (POJO – Plain Old Java Object) in JSON (JavaScript Object Notation) il formato testuale di interscambio di dati tra applicazioni, semplice e veloce da processare, ed anche come effettuare la trasformazione inversa, da una stringa di testo in formato JSON rappresentante le informazioni dell’oggetto, all’oggetto Java stesso (deserializzazione).

Per farlo creiamo qualche semplice classe con la quale modelliamo un sistema di gestione di ordini di un sito di e-commerce. Avremo quindi gli ordini, i clienti, gli oggetti inclusi nell’ordine, con la loro quantità, prezzo, ecc..
Per prima cosa definiamo quindi le classi che ci servono. Iniziamo dalla classe Order che conterrà l’identificativo dell’ordine e del Customer che ha effettuato l’ordine, la lista delle righe che costituiscono l’ordine, l’importo totale dell’ordine e la data in cui l’ordine è stato inserito.

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

public class Order {

    private long id;
    private Customer customer;
    private List itemList;
    private double total;
    private Date placedDate;

    // Conosco già il totale per cui lo uso nel costruttore
    public Order(long id, Customer cust, List li, Date pDate, double total) {
        this.id = id;
        this.customer = cust;
        this.itemList = li;
        this.placedDate = pDate;
        this.total = total;
    }

    // Questo costruttore "calcola" il totale sommando i vari item dell'ordine
    public Order(long id, Customer cust, List li, Date pDate) {
        this.id = id;
        this.customer = cust;
        this.placedDate = pDate;

        // this.itemList = li;
        // this.total = this.total;
        for (OrderItem oi : li) {
            this.addItemToOrder(oi);
        }
    }

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

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

    public Customer getCustomer() {
        return this.customer;
    }

    public void setCustomer(Customer cust) {
        this.customer = cust;
    }

    public List getItemList() {
        return this.itemList;
    }

    public void setItemList(List itemList) {
        this.itemList = itemList;
    }

    public void addItemToOrder(OrderItem oi) {
        if (this.itemList == null) {
            this.itemList = new ArrayList<>();
        }
        this.itemList.add(oi);
        this.total += oi.getPrice();
    }

    public Date getPlacedDate() {
        return this.placedDate;
    }

    public void setPlacedDate(Date placedDate) {
        this.placedDate = placedDate;
    }

    public double getTotal() {
        return this.total;
    }

    public void setTotal(double total) {
        this.total = total;
    }

    @Override
    public String toString() {
        StringBuilder ret = new StringBuilder();
        ret.append("ORDER ID: ").append(this.id).append("\n");
        ret.append("ORDER DATE: ").append(this.placedDate).append("\n");
        ret.append("CUSTOMER: ").append(this.customer).append("\n");
        ret.append("ITEMS:\n");
        for (OrderItem oi : this.itemList) {
            ret.append("\t").append(oi).append("\n");
        }
        ret.append("TOTAL: ").append(this.total).append("\n");
        return ret.toString();
    }
}

Abbiamo ridefinito il metodo toString in modo da ottenere una rappresentazione testuale esplicativa dei nostri oggetti Order.

A questo punto dobbiamo definire la classe OrderItem che rappresenta le singole righe di un ordine che, a loro volta, saranno costituite dall’articolo acquistato, dalla relativa quantità di pezzi e dal prezzo totale per questa riga dell’ordine, dato dalla moltiplicazione tra la quantità ed il prezzo unitario dell’articolo.

public class OrderItem {

    private int quantity;
    private Item it;
    private double price;

    public OrderItem(int q, Item it){
        this.quantity = q;
        this.it = it;
        this.price = q*it.getPrice();
    }

    public int getQuantity() {
        return this.quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }

    public Item getIt() {
        return this.it;
    }

    public void setIt(Item it) {
        this.it = it;
    }

    public double getPrice() {
        return this.price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString(){
        return "n∞ " + this.quantity + " " + this.it + ": " + this.price;
    }
}

Proseguiamo nella definizione delle classi con Item, che rappresenta semplicemente un articolo disponibile per l’acquisto. I suoi campi saranno quindi un identificativo, il nome dell’oggetto stesso, la sua categoria di appartenenza ed il suo prezzo unitario. La categoria la rappresentiamo tramite un enumeratore che andremo a definire subito dopo.

public class Item {
    private long id;
    private String name;
    private Categories cat;
    private double price;

    public Item (long id, String n, Categories c, double p){
        this.id = id;
        this.name = n;
        this.cat = c;
        this.price = p;
    }

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

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

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

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

    public Categories getCat() {
        return this.cat;
    }

    public void setCat(Categories cat) {
        this.cat = cat;
    }

    public double getPrice() {
        return this.price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString(){
        return this.name + " (unit price: " + this.price + " - cat: " + this.cat + ")" ;
    }
}

Definiamo quindi l’enumeratore delle categorie:

public enum Categories {
    SPORT, BOOK, HARDWARE
}

Infine, ci resta più solo da definire la classe Customer, che rappresenta i clienti, che identifichiamo semplicemente tramite un id ed il loro nome e cognome.

public class Customer {

    private long id;
    private String firstName;
    private String lastName;


    public Customer(long id, String fn, String ln) {
        this.firstName = fn;
        this.lastName = ln;
        this.id = id;
    }

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

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

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
            return this.firstName + " " + this.lastName;
    }
}

Ora che abbiamo definito le classi necessarie, creiamo un programmino di test che crea un oggetto Order e ce lo stampa a video:

import java.util.ArrayList;
import java.util.Date;

public class JacksonExample {

    public static void main(String[] args) {
        Customer c1 = new Customer(1,"Davis", "Molinari");

        Item i1 = new Item(1, "Tablet XYZ", Categories.HARDWARE, 199.0);
        Item i2 = new Item(2, "Jackson Tutorial", Categories.BOOK, 19.00);
        Item i3 = new Item(3, "Running shoes", Categories.SPORT, 65.50);

        OrderItem oi1 = new OrderItem(2,i1);
        OrderItem oi2 = new OrderItem(3,i2);
        OrderItem oi3 = new OrderItem(1,i3);

        Order o = new Order(1000, c1, new ArrayList(), new Date());
        o.addItemToOrder(oi1);
        o.addItemToOrder(oi2);
        o.addItemToOrder(oi3);

        System.out.println(o);
    }
}

Provando ad eseguire il programma otteniamo il seguente output:

ORDER ID: 1000
ORDER DATE: Mon Dec 15 15:47:39 CET 2014
CUSTOMER: Davis Molinari
ITEMS:
	n∞ 2 Tablet XYZ (unit price: 199.0 - cat: HARDWARE): 398.0
	n∞ 3 Jackson Tutorial (unit price: 19.0 - cat: BOOK): 57.0
	n∞ 1 Running shoes (unit price: 65.5 - cat: SPORT): 65.5
TOTAL: 520.5

Ok, ora torniamo all’obbiettivo principale, cioè quello di fornire un esempio di serializzazione JSON con Jackson.
Per prima cosa aggiungiamo quindi le librerie Jackson al Build Path del nostro progetto; i jar da includere, come illustato nell figura seguente, sono quelli dei 3 moduli in cui il progetto Jackson è stato suddiviso:

– Core
– DataBind
– Annotations

Adding Jackson jars to build path

Una volta aggiunte le librerie al progetto, modifichiamo il nostro programma in modo da eseguire la serializzazione JSON del nostro oggetto Order. Quello che dobbiamo fare è creare un oggetto ObjectMapper ed invocare su di esso il metodo writeValue passandogli come parametri lo stream su cui scrivere il valore (nel nostro caso System.out, lo standard output) e l’oggetto da serializzare (il nostro oggetto Order).
Ecco il nuovo codice del nostro main:

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;

import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

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

        Customer c1 = new Customer(1,"Davis", "Molinari");

        Item i1 = new Item(1, "Tablet XYZ", Categories.HARDWARE, 199.0);
        Item i2 = new Item(2, "Jackson Tutorial", Categories.BOOK, 19.00);
        Item i3 = new Item(3, "Running shoes", Categories.SPORT, 65.50);

        OrderItem oi1 = new OrderItem(2,i1);
        OrderItem oi2 = new OrderItem(3,i2);
        OrderItem oi3 = new OrderItem(1,i3);

        Order o = new Order(1000, c1, new ArrayList(), new Date());
        o.addItemToOrder(oi1);
        o.addItemToOrder(oi2);
        o.addItemToOrder(oi3);

        try {
            mapper.writeValue(System.out, o);
        }
        catch (JsonGenerationException 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();
        }
    }
}

Di seguito vediamo la (lunghissima) stringa risultante dal processo di serializzazione:

{"id":1000,"itemList":[{"quantity":2,"it":{"id":1,"name":"Tablet XYZ","cat":"HARDWARE","price":199.0},"price":398.0},{"quantity":3,"it":{"id":2,"name":"Jackson Tutorial","cat":"BOOK","price":19.0},"price":57.0},{"quantity":1,"it":{"id":3,"name":"Running shoes","cat":"SPORT","price":65.5},"price":65.5}],"placedDate":1419257720888,"cust":{"id":1,"firstName":"Davis","lastName":"Molinari"}}

Visualizzata in questo modo la stringa non è per niente semplice da analizzare, per cui indichiamo, solo per fini di verifica, all’oggetto ObjectMapper di formattarci l’output in un modo un po’ più leggibile. Per farlo effettuiamo sul mapper la chiamata seguente:

mapper.configure(SerializationFeature.INDENT_OUTPUT, true);

oppure, in alternativa:

mapper.enable(SerializationFeature.INDENT_OUTPUT);

Rieseguendo il nostro programma di test otteniamo questa volta la stringa formattata nel modo seguente:

{
  "id" : 1000,
  "customer" : {
    "id" : 1,
    "firstName" : "Davis",
    "lastName" : "Molinari"
  },
  "itemList" : [ {
    "quantity" : 2,
    "it" : {
      "id" : 1,
      "name" : "Tablet XYZ",
      "cat" : "HARDWARE",
      "price" : 199.0
    },
    "price" : 398.0
  }, {
    "quantity" : 3,
    "it" : {
      "id" : 2,
      "name" : "Jackson Tutorial",
      "cat" : "BOOK",
      "price" : 19.0
    },
    "price" : 57.0
  }, {
    "quantity" : 1,
    "it" : {
      "id" : 3,
      "name" : "Running shoes",
      "cat" : "SPORT",
      "price" : 65.5
    },
    "price" : 65.5
  } ],
  "total" : 520.5,
  "placedDate" : 1419260209564
}

Come possiamo vedere il nostro oggetto Order è rappresentato da un array associativo di coppie chiave-valore, racchiuse tra parentesi graffe. Il primo campo, “id” è un tipo primitivo per cui viene semplicemente riportato il suo valore. Il campo “customer” invece, è costituito nel nostro oggetto Java da un oggetto della classe Customer, per cui nel processo di serializzazione viene definito come un nuovo oggetto, aprendo una nuova parentesi graffa, all’interno della quale vengono rappresentati gli attributi dell’oggetto Customer. Il successivo campo della classe Order da rappresentare nella stringa JSON è “itemList” che, a livello di classe è definito come una list di OrderItem. Le liste in JSON sono rappresentate tra parentesi quadre, per cui nella nostra stringa di serializzazione abbiamo il campo “itemList” valorizzato utilizzando una coppia di parentesi quadre all’interno delle quali vengono elencate, separate da virgola, le rappresentazioni degli oggetti OrderItem. Ogni elemento OrderItem, essendo un oggetto della classe, è racchiuso tra parentesi graffe e contiene i campi “quantity”, “it” racchiuso tra graffe perchè, nuovamente, rappresenta un oggetto Item, e “price”. Infine l’oggetto Order prevede gli ultimi due campi semplici, “total” e “placedDate”.
Proprio su quest’ultimo campo, rappresentante la data in cui l’ordine è stato inserito, notiamo qualcosa di strano. La data è rappresentata nel formato “era dei computer” ovvero come numero di millisecondi trascorsi dal 01-01-1970. Per dargli una rappresentazione più leggibile dobbiamo intervenire, al solito, sul nostro oggetto ObjectMapper settandogli il formato che vogliamo.

SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy");
mapper.setDateFormat(sdf); 

Eseguiamo nuovamente il nostro programma di test per la serializzazione del nostro oggetto Order ed otteniamo questa volta il risultato seguente:

{
  "id" : 1000,
  "customer" : {
    "id" : 1,
    "firstName" : "Davis",
    "lastName" : "Molinari"
  },
  "itemList" : [ {
    "quantity" : 2,
    "it" : {
      "id" : 1,
      "name" : "Tablet XYZ",
      "cat" : "HARDWARE",
      "price" : 199.0
    },
    "price" : 398.0
  }, {
    "quantity" : 3,
    "it" : {
      "id" : 2,
      "name" : "Jackson Tutorial",
      "cat" : "BOOK",
      "price" : 19.0
    },
    "price" : 57.0
  }, {
    "quantity" : 1,
    "it" : {
      "id" : 3,
      "name" : "Running shoes",
      "cat" : "SPORT",
      "price" : 65.5
    },
    "price" : 65.5
  } ],
  "total" : 520.5,
  "placedDate" : "15 dic 2014"
}

Come possiamo vedere dall’output generato, la data viene questa volta rappresentata con il formato specificato e risulta quindi leggibile:

"placedDate" : "15 dic 2014"

Passiamo ora ad analizzare il processo di deserializzazione. Per farlo dobbiamo prima fare una modifica al nostro programma di test in modo da fagli scrivere la stringa contenente la serializzazione JSON del nostro Order su una String invece di stamparlo a video. Per tale scopo sostituiamo l’invocazione del metodo writeValue sul nostro ObjectMapper con l’invocazione del metodo writeValueAsString che ci restituisce appunto il JSON come String:

import java.util.Date;

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

public class JacksonExample {

    public static void main(String[] args) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
        
	SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy");
        mapper.setDateFormat(sdf);

        Customer c1 = new Customer(1,"Davis", "Molinari");

        Item i1 = new Item(1, "Tablet XYZ", Categories.HARDWARE, 199.0);
        Item i2 = new Item(2, "Jackson Tutorial", Categories.BOOK, 19.00);
        Item i3 = new Item(3, "Running shoes", Categories.SPORT, 65.50);

        OrderItem oi1 = new OrderItem(2,i1);
        OrderItem oi2 = new OrderItem(3,i2);
        OrderItem oi3 = new OrderItem(1,i3);

        Order o = new Order(1000, c1, new ArrayList(), new Date());
        o.addItemToOrder(oi1);
        o.addItemToOrder(oi2);
        o.addItemToOrder(oi3);

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

    }
}

Il risultato ottenuto è lo stesso di prima, quando scrivevamo direttamente il JSON sullo standard output con il metodo writeValue.

A questo punto abbiamo il nostro JSON salvato in una String, per cui possiamo provare a farne la deserializzazione utilizzando il metodo readValue.
Modifichiamo il nostro programma di test in modo da fargli creare un oggetto java Order a partire dalla sua rappresentazione JSON salvata nella stringa.

package dede.example;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

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 JacksonExample {

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

        SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy");
        mapper.setDateFormat(sdf);

        Customer c1 = new Customer(1,"Davis", "Molinari");

        Item i1 = new Item(1, "Tablet XYZ", Categories.HARDWARE, 199.0);
        Item i2 = new Item(2, "Jackson Tutorial", Categories.BOOK, 19.00);
        Item i3 = new Item(3, "Running shoes", Categories.SPORT, 65.50);

        OrderItem oi1 = new OrderItem(2,i1);
        OrderItem oi2 = new OrderItem(3,i2);
        OrderItem oi3 = new OrderItem(1,i3);

        Order o = new Order(1000, c1, new ArrayList(), new Date());
        o.addItemToOrder(oi1);
        o.addItemToOrder(oi2);
        o.addItemToOrder(oi3);

        System.out.println(o);
        System.out.println("------------");

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

        System.out.println(s);
        System.out.println("------------");

        Order o2 = null;
        try {
            o2 = mapper.readValue(s, Order.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();
        }

        System.out.println(o2);
    }
}

Eseguendo il programma otteniamo questo risultato:

com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type
[simple type, class dede.example.Order]: can not instantiate from JSON object
(need to add/enable type information?)

Questo significa che dobbiamo indicare quale costruttore vogliamo utilizzare per istanziare un oggetto della classe Order, partendo dalla sua rappresentazione JSON. Per farlo dobbiamo annotare il costruttore da utilizzare con l’annotation di Jackson “@JsonCreator” ed annotare ogni parametro del costruttore con l’annotation di Jackson “@JsonProperty” abbinata alla proprietà della classe che quel parametro mappa.

Modifichiamo quindi il costruttore della classe Order nel modo seguente:

    @JsonCreator
    public Order(@JsonProperty("id")long id, @JsonProperty("customer")Customer cust, @JsonProperty("itemList")List li, @JsonProperty("placedDate")Date pDate, @JsonProperty("total")double total) {
        this.id = id;
        this.customer = cust;
        this.itemList = li;
        this.placedDate = pDate;
        this.total = this.total;
    }

Provando ad eseguire nuovamente il programma di test otteniamo questa volta:

com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type
[simple type, class dede.example.Customer]: can not instantiate from JSON object 
(need to add/enable type information?)

Come possiamo vedere, questa volta l’errore si è “spostato” sul costruttore della classe Customer, per cui la configurazione che abbiamo fornito per il costruttore della classe Order era corretta. Ripetiamo la stessa operazione di configurazione tramite le annotations di Jackson per i costruttori di tutte le nostre classi. Essi diventano quindi come segue:

    @JsonCreator
    public Customer(@JsonProperty("id")long id, @JsonProperty("firstName")String fn, @JsonProperty("lastName")String ln) {
        this.firstName = fn;
        this.lastName = ln;
        this.id = id;
    }
    @JsonCreator
    public OrderItem(@JsonProperty("quantity")int q, @JsonProperty("it")Item it){
        this.quantity = q;
        this.it = it;
        this.price = q*it.getPrice();
    }
    @JsonCreator
    public Item (@JsonProperty("id")long id, @JsonProperty("name")String n, @JsonProperty("cat")Categories c, @JsonProperty("price")double p){
        this.id = id;
        this.name = n;
        this.cat = c;
        this.price = p;
    }

Una volta configurati tutti i costruttori da utilizzare in fase di desrializzazione, eseguiamo nuovamente il nostro programma di test ed otteniamo il risultato seguente:

ORDER ID: 1000
ORDER DATE: Mon Dec 15 15:47:39 CET 2014
CUSTOMER: Davis Molinari
ITEMS:
	n∞ 2 Tablet XYZ (unit price: 199.0 - cat: HARDWARE): 398.0
	n∞ 3 Jackson Tutorial (unit price: 19.0 - cat: BOOK): 57.0
	n∞ 1 Running shoes (unit price: 65.5 - cat: SPORT): 65.5
TOTAL: 520.5

------------
{
  "id" : 1000,
  "customer" : {
    "id" : 1,
    "firstName" : "Davis",
    "lastName" : "Molinari"
  },
  "itemList" : [ {
    "quantity" : 2,
    "it" : {
      "id" : 1,
      "name" : "Tablet XYZ",
      "cat" : "HARDWARE",
      "price" : 199.0
    },
    "price" : 398.0
  }, {
    "quantity" : 3,
    "it" : {
      "id" : 2,
      "name" : "Jackson Tutorial",
      "cat" : "BOOK",
      "price" : 19.0
    },
    "price" : 57.0
  }, {
    "quantity" : 1,
    "it" : {
      "id" : 3,
      "name" : "Running shoes",
      "cat" : "SPORT",
      "price" : 65.5
    },
    "price" : 65.5
  } ],
  "placedDate" : "07 gen 2015",
  "total" : 520.5
}
------------
ORDER ID: 1000
ORDER DATE: Mon Dec 15 00:00:00 CET 2014
CUSTOMER: Davis Molinari
ITEMS:
	n∞ 2 Tablet XYZ (unit price: 199.0 - cat: HARDWARE): 398.0
	n∞ 3 Jackson Tutorial (unit price: 19.0 - cat: BOOK): 57.0
	n∞ 1 Running shoes (unit price: 65.5 - cat: SPORT): 65.5
TOTAL: 520.5

Il risultato, come si può vedere, mostra a video:
– il toString dell’oggetto java della classe Order
– la rappresentazione JSON dell’oggetto ottenuta dal processo di serializzazione
– il toString del nuovo oggetto java della classe Order creato tramite la deserializzazione della stringa JSON

Il codice completo dell’esempio è disponibile per il download qui:

3 thoughts on “Serializzazione e deserializzazione JSON di oggetti Java con Jackson: un esempio concreto

  1. Pingback: JSON serialization and deserialization of JAVA objects with Jackson: a concrete example | Dede Blog

  2. Pingback: Jackson: creare e registrare un serializzatore JSON custom con le classi StdSerializer e SimpleModule | Dede Blog

  3. Pingback: Jackson JSON: usare l’annotazione @JsonPropertyOrder per definire l’ordine di serializzazione delle proprietà | Dede Blog

Leave a Reply

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