Jackson: escludere le proprietà con valore null dalla serializzazione JSON

Nei commenti ad un precedente articolo sulla libreria Jackson (in inglese), era stata sollevata la domanda relativa a come escludere dalla serializzazione JSON di un oggetto Java le proprietà che avevano un valore null. Siccome non era la prima volta che arrivava una
richiesta di chiarimento sull’argomento, ho deciso di affrontarlo in dettaglio in questo nuovo post.

Jackson offre principalmente due alternative per escludere i valori null in fase di serializzazione JSON:

  • A livello di singola classe, utilizzando l’annotation @JsonInclude con il valore Include.NON_NULL
  • A livello di ObjectMapper utilizzando il metodo setSerializationInclusion(JsonInclude.Include.NON_NULL)

Utilizzando il primo metodo si agisce a livello di classe, mentre con il secondo metodo si imposta tale comportamento a livello globale e lo si rende attivo per tutte le classi serializzate utilizzando quel ObjectMapper. Vediamo ora un esempio di applicazione di queste due diverse strategie, riprendendo le classi già utilizzate in questo altro articolo dove avevamo analizzato in dettaglio i processi di serializzazione e deserializzazione JSON. Riutilizziamo quindi le classi Order, OrderItem, Customer, ecc.. con cui avevamo rappresentato, in forma semplificata, un sistema di gestione degli ordini:
La classe Customer è definita come segue:

public class Customer {

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

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

	// GETTERS E SETTERS OMESSI PER SEMPLICITA'

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

La classe Order è definita come segue:

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

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

public class Order {

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

    @JsonCreator
    public Order(@JsonProperty("id") long id, @JsonProperty("cust") Customer cust, @JsonProperty("itemList")List li, @JsonProperty("placedDate")Date pDate) {
        this.id = id;
        this.customer = cust;
        for (OrderItem oi : li) {
            this.addItemToOrder(oi);
        }
        this.placedDate = pDate;
    }


    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 = this.total;
    }

	// GETTERS E SETTERS OMESSI PER SEMPLICITA'
	
    @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd,HH:mm", timezone="CET")
    private Date placedDate;

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

    @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();
    }
}

La classe OrderItem è definita come segue:

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

public class OrderItem {

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

    @JsonCreator
    public OrderItem(@JsonProperty("quantity")int q, @JsonProperty("it")Item it, @JsonProperty("price")double p){
        this.quantity = q;
        this.it = it;
        this.price = p;
    }

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

	// GETTERS AND SETTERS OMITTED 

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

La classe Item è definita come segue:

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

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

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

	// GETTERS AND SETTERS OMITTED 

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

Avevamo poi un enumerativo per le categorie dei prodotti

public enum Categories {
    SPORT, BOOK, HARDWARE
}

A questo punto avevamo realizzato una classe di test e creato un Customer ed un Order di esempio che avevamo poi serializzato.

import java.util.ArrayList;
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.enable(SerializationFeature.INDENT_OUTPUT);

        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, formattato con l’indentazione per una più facile lettura tramite l’opzione SerializationFeature.INDENT_OUTPUT, era il seguente:

{
  "id" : 1000,
  "cust" : {
    "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" : "2015-09-23,16:42",
  "total" : 520.5
}

In questo caso non erano presenti, in nessuno degli oggetti creati, delle proprietà con valore null. Modifichiamo invece ora il nostro programma di test lasciando non valorizzate alcune proprietà dei nostri oggetti; Ad esempio, nella creazione dell’utente inseriamo solo il nome senza valorizzare il cognome e lasciamo uno degli Item senza categoria:

import java.util.ArrayList;
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.enable(SerializationFeature.INDENT_OUTPUT);

        // Customer c1 = new Customer(1,"Davis", "Molinari");
        Customer c1 = new Customer(1,"Davis", null);		// lastName è null

        // Item i1 = new Item(1, "Tablet XYZ", Categories.HARDWARE, 199.0);
        Item i1 = new Item(1, "Tablet XYZ", null, 199.0);	// category è null
        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 che otteniamo questa volta dal processo di serializzazione è il seguente:

{
  "id" : 1000,
  "cust" : {
    "id" : 1,
    "firstName" : "Davis",
    "lastName" : null
  },
  "itemList" : [ {
    "quantity" : 2,
    "it" : {
      "id" : 1,
      "name" : "Tablet XYZ",
      "cat" : null,
      "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" : "2015-09-24,12:53",
  "total" : 520.5
}

Come possiamo vedere, questa volta le proprietà “lastName” del Customer e la “cat” del primo Item inserito nell’ordine sono null. Il nostro scopo è quindi quello di evitare che tali proprietà, che sono valorizzate a null, compaiano nella stringa JSON serializzata.
Vediamo come farlo nei due modi descritti ad inizio articolo:

1) Esclusione a livello di classe con annotazione @JsonInclude(Include.NON_NULL)
Il primo metodo consiste nell’utilizzo dell’annotazione @JsonInclude, alla quale viene specificato di escludere i valori null tramite la direttiva Include.NON_NULL.
Per verificare il suo funzionamento modifichiamo la classe Customer aggiungendo questa annotazione:

@JsonInclude(Include.NON_NULL)
public class Customer {

	// CONTENUTO DELLA CLASSE OMESSO
	
}

Eseguendo nuovamente il programma di test, otteniamo questa volta il risultato seguente:

{
  "id" : 1000,
  "cust" : {
    "id" : 1,
    "firstName" : "Davis"
  },
  "itemList" : [ {
    "quantity" : 2,
    "it" : {
      "id" : 1,
      "name" : "Tablet XYZ",
      "cat" : null,
      "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" : "2015-09-24,15:42",
  "total" : 520.5
}

Come possiamo vedere, questa volta le proprietà dell’oggetto Customer che erano a null non sono state incluse nella stringa JSON di serializzazione. Non è infatti presente, a differenza di prima, la riga:

"lastName" : null

In questo modo, comunque, abbiamo agito a livello di classe ed avendo annotato con @JsonInclude solo la classe Customer abbiamo escluso solo le sue proprietà null. Possiamo infatti vedere che è ancora presente la riga:

"cat" : null

perchè le proprietà con valore null della classe Item non vengono escluse dalla serializzazione.
Per escludere anche le proprietà null della classe Item dobbiamo annotare anch’essa con @JsonInclude(Include.NON_NULL)

@JsonInclude(Include.NON_NULL)
public class Item {

	// CONTENUTO DELLA CLASSE OMESSO

}

Lanciamo nuovamente il programma di test e vediamo che adesso anche la proprietà della classe Item con valore a null non compare nella stringa di serializzazione. Il primo item della lista non presenta quindi la proprietà “cat”:

{
  "id" : 1000,
  "cust" : {
    "id" : 1,
    "firstName" : "Davis"
  },
  "itemList" : [ {
    "quantity" : 2,
    "it" : {
      "id" : 1,
      "name" : "Tablet XYZ",
      "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" : "2015-09-24,17:21",
  "total" : 520.5
}


2) Esclusione a livello globale tramite configurazione dell’ObjectMapper

Passiamo ora ad analizzare la seconda soluzione, che prevede di impostare la strategia di esclusione dei valori null come comportamento comune per la serializzazione di tutte le classi, settando una proprietà dell’ObjectMapper.
Per prima cosa ripristiniamo i nostri POJOs, rimuovendo le annotazioni che avevamo inserito precedentemente nelle nostre classi.

public class Customer {

	// CONTENUTO DELLA CLASSE OMESSO
	
}
public class Item {

	// CONTENUTO DELLA CLASSE OMESSO

}

A questo punto dobbiamo modificare la classe di test, andando a specificare al nostro ObjectMapper di prendere in considerazione, in fase di serializzazione, solo le proprietà che hanno un valore diverso da null. Per farlo dobbiamo invocare sull’ObjectMapper il metodo setSerializationInclusion, passandogli come parametro JsonInclude.Include.NON_NULL.

mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

Facciamo quindi la modifica alla classe di test JacksonExample:

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

import com.fasterxml.jackson.annotation.JsonInclude;
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.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // IMPOSTA LA PROPRIETA'

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

        // Item i1 = new Item(1, "Tablet XYZ", Categories.HARDWARE, 199.0);
        Item i1 = new Item(1, "Tablet XYZ", null, 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);
    }
}

E proviamo ad eseguirla:

{
  "id" : 1000,
  "cust" : {
    "id" : 1,
    "firstName" : "Davis"
  },
  "itemList" : [ {
    "quantity" : 2,
    "it" : {
      "id" : 1,
      "name" : "Tablet XYZ",
      "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" : "2015-09-24,17:40",
  "total" : 520.5
}

Come possiamo vedere dall’output generato, nella stringa JSON non compaiono le proprietà valorizzate a null né della classe Item né della classe Customer. Agendo direttamente sull’ObjectMapper, come abbiamo detto, applichiamo il comportamento direttamente a tutte le classi.

Quindi, riassumendo, questo secondo metodo è utilizzabile quando si è sicuri che l’esclusione dei valori null è da applicare a tutte le classi serializzate con quell’ObjectMapper (Assumendo, ovviamente, di avere accesso all’ObjectMapper utilizzato). Il primo metodo, con l’annotazione a livello di singola classe, permette di definire un comportamento differente per le varie classi a seconda delle esigenze e risulta quindi più flessibile.

Leave a Reply

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