Jackson: creare un deserializzatore JSON custom con le classi StdDeserializer e JsonToken

Nel post precedente abbiamo visto come creare con Jackson un serializzatore JSON personalizzato, al fine di poter gestire nel modo desiderato gli oggetti di una determinata classe ed ottenere una loro rappresentazione diversa da quella di default. Questo nuovo articolo è la sua naturale continuazione, per cui andremo ad affrontare il procedimento inverso, creando un deserializzatore JSON custom che ci permetta di ricreare un oggetto della nostra classe, partendo proprio da una stringa che rappresenta la sua serializzazione secondo il formato ad-hoc definito tramite il serializzatore personalizzato.
Come per la creazione di un serializzatore custom Jackson ci metteva a disposizione per estensione la classe StdSerializer, analogamente per definire il relativo deserializzatore personalizzato partiremo dall’estensione della classe StdDeserializer ed implementeremo il metodo deserialize() che essa eredita dalla classe astratta JsonDeserializer. Tale metodo, è definito come segue:

public abstract T deserialize(JsonParser jp, DeserializationContext ctxt)
                       throws IOException, JsonProcessingException

e possiamo quindi vedere che restituisce un’istanza della classe T per cui è definito come deserializzatore. JsonParser è la classe che ci permette di analizzare il JSON e determinare il nome dei campi, il loro tipo e di estrarne i valori. Essa ci mette a disposizione il metodo nextValue() che ci restituisce la tipologia del successivo elemento sottoforma di valore dell’enumeratore JsonToken, ad esempio “VALUE_NUMBER_INT”, “VALUE_NUMBER_FLOAT”, “VALUE_STRING”, ecc. Una volta identificata la tipologia del token letto, possiamo controllare il suo nome per distinguire, ad esempio nel nostro caso, se si tratta dell’elemento di tipo stringa “name” oppure dell’elemento “languages” e gestire opportunamente ciascuno dei due. Nel primo caso infatti dobbiamo semplicemente leggere la stringa che rappresenta il valore del campo, mentre nel caso dell’elemento “languages” una volta letta la stringa dobbiamo splittarla in base al separatore ‘;’ e creare l’array di stringhe relative ai linguaggi conosciuti, da utilizzare per popolare il membro di istanza “languages” della nostra classe SWEngineer. Per recuperare il nome dell’elemento corrente dal JsonParser utilizziamo il metodo getCurrentName(), mentre per estrarre poi il valore dell’id, che è di tipo long nella classe Java, utilizziamo il metodo getLongValue(). Per estrarre invece i valori di tipo String utilizzeremo il metodo getText().
Alle luce di quanto detto finora il nostro deserializzatore custom per la classe SWEngineer sarà il seguente:

class CustomDeserializer extends StdDeserializer<SWEngineer>{

    public CustomDeserializer(Class<SWEngineer> t) {
        super(t);
    }

    @Override
    public SWEngineer deserialize(JsonParser jp, DeserializationContext dc)
                                                throws IOException, JsonProcessingException {

        long id = 0;
        String name = null;
        String []languages = null;
        JsonToken currentToken = null;
        while ((currentToken = jp.nextValue()) != null) {
            switch (currentToken) {
                case VALUE_NUMBER_INT:
                    if (jp.getCurrentName().equals("id")) {
                        id = jp.getLongValue();
                    }
                    break;
                case VALUE_STRING:
                    switch (jp.getCurrentName()) {
                        case "name":
                            name = jp.getText();
                            break;
                        case "languages":
                            languages = jp.getText().split(";");
                            break;
                        default:
                            break;
                    }
                    break;
                default:
                    break;
            }
        }
        return new SWEngineer(id, name, languages);
    }
}

Una volta definito il deserializzatore personalizzato per la classe SWEngineer dobbiamo, come già avevamo fatto per il serializzatore, aggiungerlo ad un SimpleModule e registrare quest’ultimo nell’ObjectMapper di Jackson che utilizziamo per serializzare e deserializzare. Se per aggiungere il serializzatore custom ad un SimpleModule avevamo utilizzato il metodo addSerializer(), analogamente ora utilizzeremo il metodo addDeserializer() nel modo seguente:

mod.addDeserializer(SWEngineer.class, new CustomDeserializer(SWEngineer.class));

Più avanti, quando creeremo la classe di test, aggiungeremo sia il serializzatore che il deserializzatore custom ad un modulo e lo registreremo su un mapper.
Prima però di procedere con la definizione di una classe di test per verificare il funzionamento del nostro deserializzatore ed, in generale, dell’intero processo di serializzazione/deserializzazione personalizzata di un oggetto della classe SWEngineer, ridefiniamo in quest’ultima il metodo toString(), in modo da ottenere una rappresentazione esplicativa dell’oggetto utile per verificare i valori dei suoi membri di istanza prima e dopo il processo.
La classe SWEngineer con la ridefinizione del toString diventa quindi come segue:

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

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("ID: ").append(this.id).append("\nNome: ")
                .append(this.name).append("\nLinguaggi:");

        for (String s: this.languages) {
            sb.append(" ").append(s);
        }
        return sb.toString();
    }
}

Recuperiamo, sempre dal post precedente, il serializzatore custom che avevamo creato per la classe SWEngineer:

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

A questo punto abbiamo tutto il necessario per poter scrivere la nostra classe di esempio per testare l’intero processo di serializzazione e deserializzazione.
Creiamo quindi una classe in cui istanziamo un oggetto di tipo SWEngineer, lo serializziamo in una stringa JSON con il serializzatore custom e poi deserializziamo il JSON con il nostro nuovo deserializzatore, creando un nuovo oggetto della nostra classe.

public class CustomSerDeserExample {

    public static void main (String[] args) {

        SWEngineer swe1 = new SWEngineer(1, "Mark", new String[]{"Java", "Python", "C++", "Scala"});

        ObjectMapper mapper = new ObjectMapper();

        SimpleModule mod = new SimpleModule("MyModule");
        mod.addSerializer(new CustomSerializer(SWEngineer.class));
        mod.addDeserializer(SWEngineer.class, new CustomDeserializer(SWEngineer.class));
        mapper.registerModule(mod);

        System.out.println("--- OGGETTO ORIGINALE ---\n" + swe1);

        String s = null;

        try {
            s = mapper.writeValueAsString(swe1);
        }
        catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        System.out.println("\n--- JAVA to JSON (Custom) ---\n" + s);

        SWEngineer sweOut = null;
        try {
            sweOut = mapper.readValue(s, SWEngineer.class);

        }
        catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("\n--- JSON to JAVA ---\n" + sweOut);
    }
}

Il risultato dell’esecuzione del programma di test è il seguente:

--- OGGETTO ORIGINALE ---
ID: 1
Nome: Mark
Linguaggi: Java Python C++ Scala

--- JAVA to JSON (Custom) ---
{"id":1,"name":"Mark","languages":"Java;Python;C++;Scala;"}

--- JSON to JAVA ---
ID: 1
Nome: Mark
Linguaggi: Java Python C++ Scala

Come possiamo vedere viene stampata la rappresentazione dell’oggetto originale secondo il toString() che abbiamo ridefinito, poi viene stampata la stringa JSON prodotta dal nostro serializzatore, in cui troviamo la lista di elementi dell’array rappresentata come unica stringa con i valori separati da ‘;’ ed infine troviamo la stampa dell’oggetto SWEngineer ricreato tramite il deserializzatore custom che, correttamente, coincide con quella dell’oggetto originale.

Il codice completo dell’esempio è scaricabile qui:

Leave a Reply

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