Jackson: creare e registrare un serializzatore JSON custom con le classi StdSerializer e SimpleModule

In fase di serializzazione JSON di oggetti Java a volte si può avere la necessità di modificare il comportamento di default e di fornire una determinata rappresentazione personalizzata di un oggetto di una classe o di una collection. In questo ennesimo articolo sulla libreria Jackson vediamo come implementare tutto quello che serve per creare un serializzatore custom, partendo come sempre da un esempio concreto.
Supponiamo infatti di avere una classe “SWEngineer” che rappresenta gli sviluppatori, caratterizzati da un ID, un nome ed una lista dei linguaggi di programmazione conosciuti, costituita da una array di stringhe.
L’obbiettivo è serializzare la lista dei linguaggi conosciuti dallo sviluppatore come un’unica stringa, con i valori separati da un ‘;’ invece che serializzarla come una lista di stringhe rappresentanti i diversi elementi dell’array, racchiuse da una coppa di parentesi quadre.

Un modo per farlo con Jackson consiste nell’eseguire le tre seguenti operazioni:

  • Creare una serializzatore personalizzato estendendo la classe StdSerializer di Jackson
  • Creare un oggetto della classe SimpleModule di Jackson, aggiungendogli il serializzatore custom e specificando per quale classe deve essere utilizzato
  • Registrare il modulo sull’oggetto ObjectMapper

(Un altro possibile metodo, che vediamo in un altro articolo, è utilizzando l’annotation @JsonSerialize)

Ma procediamo per passi ed iniziamo a definire la nostra classe, per la quale dovremo poi fornire il serializzatore custom:

class SWEngineer {
 
    private long id;
    private String name;
    private String[] languages;
 
    public SWEngineer(long id, String name, String[] languages) {
        this.id = id;
        this.name = name;
        this.languages = languages;
    }
 
    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 String[] getLanguages() {
        return this.languages;
    }
 
    public void setLanguages(String[] languages) {
        this.languages = languages;
    }
}

Definiamo ora una classe di esempio in cui creiamo due istanze della classe SWEngineer e le inseriamo in un ArrayList che poi andiamo a serializzare:

import java.util.ArrayList;
import java.util.List;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class CustomSerDeserExample {
 
    public static void main (String[] args) {
 
        List<SWEngineer> sweList = new ArrayList<>();
        SWEngineer swe1 = new SWEngineer(1, "Mark", new String[]{"Java", "Python"});
        SWEngineer swe2 = new SWEngineer(2, "John", new String[]{"Java", "C++", "Ruby"});
        sweList.add(swe1);
        sweList.add(swe2);
 
        ObjectMapper mapper = new ObjectMapper();
 
        String s = null;
 
        try {
            s = mapper.writeValueAsString(sweList);
        }
        catch (JsonProcessingException e) {
            e.printStackTrace();
        }
 
        System.out.println(s);
    }
}

Eseguendo questo primo programma di test otteniamo il risultato seguente, in cui possiamo notare come gli oggetti della classe SWEngineer vengano serializzati con le regole di default. In questo caso quindi, l’array contenente la lista di stringhe dei linguaggi viene rappresentato con gli elementi racchiusi tra parentesi quadre.

[{"id":1,"name":"Mark","languages":["Java","Python"]},{"id":2,"name":"John","languages":["Java","C++","Ruby"]}]

Il nostro obbiettivo è proprio quello di modificare questo comportamento di default e costruire un serializzatore JSON customizzato che rappresenti il campo “languages” degli oggetti della classe SWEngineer non come un tradizionale array, ma come un’unica stringa con i valori separati da ‘;’ (punto e virgola).
Nel JSON creato quindi, i valori:

"languages":["Java","Python"]
"languages":["Java","C++","Ruby"]

dovranno diventare

"languages":"Java;Python;"
"languages":"Java;C++;Ruby;"

Vediamo come procedere.
Per prima cosa creiamo il nostro serializzatore, definendo una classe che estende la classe StdSerializer fornita da Jackson. Tale classe prevede un metodo astratto serialize(), di cui dovremo fare l’override, definito come segue:

abstract void serialize(T value, JsonGenerator jgen, SerializerProvider provider)

Quello che ci interessa è “value” che rappresenta l’oggetto che dobbiamo serializzare e “jgen” che è un’implementazione della classe astratta JsonGenerator che crea effettivamente la stringa contenente la serializzazione dell’istanza della classe per cui stiamo scrivendo il serializzatore custom. JsonGenerator fornisce i metodi per scrivere i campi dei diversi tipi nella stringa risultato, come ad esempio writeNumberField, writeStringField, ecc..

Creiamo quindi il nostro serializzatore e ridefiniamo il metodo “serialize”:

import java.io.IOException; 
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
 
 
class CustomSerializer extends StdSerializer<SWEngineer> {
 
    public CustomSerializer(Class<SWEngineer> t) {
        super(t);
    }
 
    @Override
    public void serialize(SWEngineer swe,
                          JsonGenerator jgen,
                          SerializerProvider sp) throws IOException, JsonGenerationException {
       
        StringBuilder lang = new StringBuilder();
        jgen.writeStartObject();     
        jgen.writeNumberField("id", swe.getId());
        jgen.writeStringField("name", swe.getName());     
        
        for (String s: swe.getLanguages()) {
            lang.append(s).append(";");
        }   
        jgen.writeStringField("languages", lang.toString());
       
        jgen.writeEndObject();
    }
}

Come possiamo vedere il nostro CustomSerializer estende StdSerializer e nella ridefinizione del metodo serialize utilizziamo i metodi writeStartObject() e writeEndObject() per iniziare e terminare la stringa serializzata ed i metodi writeNumberField e writeStringField per scrivere rispettivamente i valori di “id” e “name”. La serializzazione del campo “languages” della classe SWEngineer è proprio il punto in cui dobbiamo intervenire per modificare il comportamento di default ed ottenere la concatenazione dei valori. Creiamo quindi uno StringBuilder ed eseguiamo un ciclo sui valori dell’array, concatenandoli tra loro separati da un ‘;’. Nuovamente utilizzando il metodo writeStringField scriviamo quindi la stringa risultante come valore del campo “languages”.

Prima di poter testare il nostro serializzatore, dobbiamo procedere con le altre due operazioni, cioè creare un SimpleModule a cui collegare il nostro serializzatore custom per la classe SWEngineer e registrarlo poi con l’ObjectMapper.
Per la prima basta istanziare un oggetto SimpleModule, fornendogli un nome (ed eventualmente una versione aggiungendo come secondo parametro un’istanza di Version), ed invocare su di esso il metodo addSerializer(), definito come segue:

public SimpleModule addSerializer(JsonSerializer<?> ser)

Per registrare poi il modulo creato sull’oggetto ObjectMapper basta invocare su di esso il metodo registerModule() passandogli appunto il modulo come parametro.
Modifichiamo quindi il nostro programma di test come segue:

public class CustomSerDeserExample {
 
    public static void main (String[] args) {
 
        List<SWEngineer> sweList = new ArrayList<>();
        SWEngineer swe1 = new SWEngineer(1, "Mark", new String[]{"Java", "Python"});
        SWEngineer swe2 = new SWEngineer(2, "John", new String[]{"Java","C++","Ruby"});
        sweList.add(swe1);
        sweList.add(swe2);
 
        ObjectMapper mapper = new ObjectMapper();
 
        // creo il modulo
        SimpleModule mod = new SimpleModule("SWEngineer Module");

        // aggiungo il serializzatore custom al modulo
        mod.addSerializer(new CustomSerializer(SWEngineer.class));         
                              
        mapper.registerModule(mod);        // registro il modulo sul mapper
 
        String s = null;
 
        try {
            s = mapper.writeValueAsString(sweList);
        }
        catch (JsonProcessingException e) {
            e.printStackTrace();
        }
 
        System.out.println(s);
    }
}

Eseguendo nuovamente il test otteniamo il risultato seguente:

[{"id":1,"name":"Mark","languages":"Java;Python;"},{"id":2,"name":"John","languages":"Java;C++;Ruby;"}]

Come possiamo vedere il risultato è esattamente quello che volevamo, in quanto l’attributo “languages” viene valorizzato non più come un array ma come un’unica stringa contenente le varie stringhe dell’array separate da ‘;’.
Ricordiamo che l’output precedente, con la serializzazione JSON di default, era il seguente:

[{"id":1,"name":"Mark","languages":["Java","Python"]},{"id":2,"name":"John","languages":["Java","C++","Ruby"]}]

Non temete, nel prossimo articolo vedremo come creare il relativo deserializzatore personalizzato che prenderà il valore dell’unica stringa con cui il campo “languages” è stato serializzato nel JSON e, splittandola sul separatore ‘;’ creerà le diverse stringhe con cui riempire l’array che rappresenta il campo “languages” nella classe Java SWEngineer.

L’esempio completo con tutte le classi può essere scaricato qui:

One thought on “Jackson: creare e registrare un serializzatore JSON custom con le classi StdSerializer e SimpleModule

  1. Pingback: Jackson: usare le annotations @JsonSerialize/@JsonDeserialize per registrare un serializzatore/deserializzatore custom | Dede Blog

Leave a Reply

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