CodeGym /Corsi /JAVA 25 SELF /Jackson — lettura e scrittura di JSON, annotazioni

Jackson — lettura e scrittura di JSON, annotazioni

JAVA 25 SELF
Livello 46 , Lezione 1
Disponibile

1. Introduzione a Jackson

Lo scambio di dati tra un’applicazione Java e il mondo esterno molto spesso può essere descritto con questa domanda: “Come trasformare un oggetto Java in JSON — e viceversa?” Certo, si potrebbe scrivere un parser con String.split e le espressioni regolari (e perfino provare il piacere della sofferenza), ma nei progetti reali non lo fa nessuno.

In Java esistono diverse librerie popolari per lavorare con JSON. La principale è Jackson. È così popolare da essere inclusa in Spring Boot ed è usata in molti altri framework e librerie.

Che cos’è Jackson?

Jackson è una libreria potente e flessibile per la serializzazione (conversione di un oggetto Java in JSON) e la deserializzazione (operazione inversa). È composta da vari moduli, ma per il 90% dei casi ve ne serviranno due:

  • jackson-core — il core, parser di basso livello.
  • jackson-databind — il modulo di alto livello che sa trasformare gli oggetti Java in JSON e viceversa.

Tutto ciò che serve sapere per iniziare: se vedete la classe ObjectMapper — è Jackson.

Jackson è considerato lo standard de facto per lavorare con JSON in Java perché unisce semplicità e potenza. Per serializzare o deserializzare i dati bastano letteralmente poche righe di codice, il che rende la libreria comoda anche per i principianti. Allo stesso tempo non si limita alle funzionalità di base: grazie alle annotazioni e alle numerose impostazioni è possibile controllare in modo flessibile come i dati vengono trasformati in oggetti e viceversa, che si tratti di collezioni, entità annidate o date in formati diversi.

È importante anche che Jackson sia molto veloce ed efficiente, cosa cruciale per i progetti reali con grandi volumi di dati. Gli sviluppatori della libreria supportano le nuove versioni di Java e gli aggiornamenti allo standard JSON — Jackson rimane una scelta affidabile sia per le applicazioni semplici sia per i grandi sistemi enterprise.

2. Lettura di JSON (deserializzazione)

Proviamo a leggere una stringa JSON e a trasformarla in un oggetto Java. Ci serviranno:

  • Una classe di dati (ad esempio, Person)
  • La classe ObjectMapper di Jackson

Aggiungere Jackson

Se usate Maven, aggiungete in pom.xml:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.0</version>
</dependency>

Se usate Gradle — in modo analogo:

implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'

Esempio di classe

public class Person {
    public String name;
    public int age;
}

Nota: Per semplicità i campi sono pubblici. Più avanti parleremo della gestione dei campi privati e dei getter/setter.

Esempio di JSON

{
  "name": "Alice",
  "age": 30
}

Deserializzazione: trasformiamo il JSON in un oggetto

import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
    public static void main(String[] args) throws Exception {
        String json = "{\"name\": \"Alice\", \"age\": 30}";

        ObjectMapper mapper = new ObjectMapper();
        Person person = mapper.readValue(json, Person.class);

        System.out.println(person.name); // Alice
        System.out.println(person.age);  // 30
    }
}

Qui Jackson analizza la stringa JSON, trova i campi con lo stesso nome dei campi o dei getter della classe e riempie l’oggetto con i valori corrispondenti tramite readValue.

Deserializzazione di una lista di oggetti

Supponiamo di avere un array:

[
  { "name": "Bob", "age": 22 },
  { "name": "Eve", "age": 27 }
]

Deserializziamo in una lista:

import com.fasterxml.jackson.core.type.TypeReference;
// ...

String json = "[{\"name\": \"Bob\", \"age\": 22}, {\"name\": \"Eve\", \"age\": 27}]";
ObjectMapper mapper = new ObjectMapper();

List<Person> people = mapper.readValue(json, new TypeReference<List<Person>>() {});

for (Person p : people) {
    System.out.println(p.name + " (" + p.age + ")");
}

Forse vi chiedete: perché non si può semplicemente scrivere mapper.readValue(json, List.class)? Ricordate che i generics in Java vengono cancellati in fase di compilazione? Per questo abbiamo bisogno di TypeReference, così Jackson capisce che all’interno della lista devono esserci oggetti di tipo Person.

3. Scrittura di JSON (serializzazione)

Ora facciamo l’operazione inversa: trasformiamo un oggetto Java in una stringa JSON.

Esempio

ObjectMapper mapper = new ObjectMapper();

Person person = new Person();
person.name = "Charlie";
person.age = 40;

String json = mapper.writeValueAsString(person);
System.out.println(json);
// {"name":"Charlie","age":40}

Serializzazione di una lista di oggetti

ObjectMapper mapper = new ObjectMapper();

List<Person> people = new ArrayList<>();
people.add(new Person("Anna", 25));
people.add(new Person("Dmitry", 31));

String json = mapper.writeValueAsString(people);
System.out.println(json);
// [{"name":"Anna","age":25},{"name":"Dmitry","age":31}]

Scrittura su file

ObjectMapper mapper = new ObjectMapper();

Person person = new Person();
person.name = "Charlie";
person.age = 40;

mapper.writeValue(new File("person.json"), person);
// Il file person.json ora contiene l’oggetto JSON

JSON formattato (pretty)

Per impostazione predefinita Jackson scrive tutto su una sola riga. Ma esiste un’opzione più “umana” — pretty printing. Significa che la stringa JSON viene prodotta in forma leggibile: con rientri, interruzioni di riga e una formattazione ordinata.

A differenza del JSON “normale”, che di solito è su una sola riga per compattezza, la variante “formattata” è pensata per le persone — per ispezionare facilmente la struttura dei dati nei log, nei file o a schermo.

ObjectMapper mapper = new ObjectMapper();

Person person = new Person();
person.name = "Charlie";
person.age = 40;

String prettyJson = mapper.writerWithDefaultPrettyPrinter()
                          .writeValueAsString(person);

System.out.println(prettyJson);
/*
{
  "name" : "Charlie",
  "age" : 40
}
*/

4. Annotazioni di Jackson

Jackson supporta molte annotazioni che permettono di controllare il processo di serializzazione e deserializzazione. Ecco le più utili:

@JsonProperty

Consente di specificare il nome del campo nel JSON, se differisce dal nome del campo nella classe.

import com.fasterxml.jackson.annotation.JsonProperty;

public class Person {
    @JsonProperty("full_name")
    public String name;

    public int age;
}
{"full_name": "Olga", "age": 28}

Jackson capirà che il campo full_name del JSON va mappato sul campo name dell’oggetto.

@JsonIgnore

Se non volete serializzare o deserializzare un certo campo:

import com.fasterxml.jackson.annotation.JsonIgnore;

public class Person {
    public String name;

    @JsonIgnore
    public int age;
}

Nel JSON non ci sarà il campo age, anche se esiste nell’oggetto.

@JsonInclude

Controlla quali campi finiranno nel JSON. Ad esempio, serializzare solo i campi non null:

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
    public String name;
    public Integer age;
}

Se age == null, nel JSON non ci sarà la chiave "age".

@JsonFormat

Permette di impostare il formato per serializzare date e orari.

import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;

public class Event {
    public String title;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    public Date date;
}
Event event = new Event();
event.title = "Hackathon";
event.date = new Date();

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(event);
// {"title":"Hackathon","date":"2024-06-07 15:23:00"}

Esempio: tutto insieme

import com.fasterxml.jackson.annotation.*;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
    @JsonProperty("full_name")
    private String name;

    private int age;

    @JsonIgnore
    private String password;

    // I getter e i setter sono obbligatori per i campi privati!
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

5. Pratica: serializzazione e deserializzazione con le annotazioni

Estendiamo la vostra applicazione didattica — ora abbiamo una classe User con campi privati, una data di registrazione e una password che non deve finire nel JSON.

import com.fasterxml.jackson.annotation.*;
import java.util.Date;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
    @JsonProperty("login")
    private String username;

    private int age;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    private Date registered;

    @JsonIgnore
    private String password;

    // Getter e setter obbligatori!
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    public Date getRegistered() { return registered; }
    public void setRegistered(Date registered) { this.registered = registered; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
}

Serializzazione

import com.fasterxml.jackson.databind.ObjectMapper;
import java.text.SimpleDateFormat;

ObjectMapper mapper = new ObjectMapper();

User user = new User();
user.setUsername("superuser");
user.setAge(42);
user.setRegistered(new SimpleDateFormat("yyyy-MM-dd").parse("2024-06-07"));
user.setPassword("qwerty123"); // Non finirà nel JSON!

String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);

System.out.println(json);
/*
{
  "login" : "superuser",
  "age" : 42,
  "registered" : "2024-06-07"
}
*/

Deserializzazione

import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();

String json = "{ \"login\": \"superuser\", \"age\": 42, \"registered\": \"2024-06-07\" }";
User user = mapper.readValue(json, User.class);

System.out.println(user.getUsername()); // superuser
System.out.println(user.getAge());      // 42
System.out.println(user.getRegistered());// Fri Jun 07 00:00:00 ...
System.out.println(user.getPassword()); // null (ed è una cosa positiva!)

6. Errori tipici nell’uso di Jackson

Errore n. 1: manca un costruttore senza argomenti.
Jackson non riuscirà a creare un’istanza della classe se non ha un costruttore senza argomenti. Succede spesso nelle classi in cui è dichiarato esplicitamente solo un costruttore con argomenti.

Errore n. 2: campi privati senza getter/setter.
Se avete reso tutti i campi privati ma avete dimenticato di aggiungere i getter e i setter, Jackson non potrà valorizzarli in deserializzazione (per impostazione predefinita).

Errore n. 3: nomi dei campi non corrispondenti.
Quando il nome del campo nel JSON differisce dal nome del campo/getter in Java, Jackson non troverà la corrispondenza. Usate @JsonProperty.

Errore n. 4: formato data errato.
Se il formato della data nel JSON non coincide con quello atteso in Java, Jackson restituirà un errore di parsing. Usate @JsonFormat per configurarlo.

Errore n. 5: tentativo di serializzare campi annotati con @JsonIgnore.
Tali campi non finiranno nel JSON — non è un bug, ma una feature.

Errore n. 6: serializzazione/deserializzazione di collezioni senza specificare il tipo.
Se non si usa TypeReference, Jackson non capirà di che tipo sono gli oggetti all’interno della collezione.

Errore n. 7: eccezione in lettura/scrittura del file.
Non dimenticate di gestire IOException quando lavorate con i file.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION