Capitolo 3 · Sviluppo web con Spring Boot: il corso pratico
Restituisci un paziente da un metodo Java, apri l'indirizzo nel browser e compare del JSON, con le sue chiavi e i suoi valori al posto giusto. Nessuno ha scritto una riga per ottenerlo.
Ci sono cose che funzionano così bene da diventare sospette. Questa è una di quelle: scrivi una classe Paziente, la restituisci e dall'altra parte arriva un documento con le chiavi, i valori, le virgole al posto giusto. Dov'è il trucco?
Un tempo il JSON si costruiva a mano, concatenando stringhe e variabili Java, e bastava un apice dimenticato per mandare in crash il client. Oggi non scrivi una riga per convertire un oggetto in JSON o viceversa.
Spoiler: il trucco c'è, ed è tutto sulla porta d'ingresso e su quella d'uscita del tuo controller. Non serve altra teoria che quella per capire dove sta.
Il modello: la classe che descrive i dati
Prima di tradurre qualcosa, serve qualcosa da tradurre. Immagina di dover registrare i pazienti del pronto soccorso: ognuno ha un nome, un cognome e un'età. In Java questo si traduce in una classe, che si chiama modello e di solito sta in un package a parte (models, per ordine).
La classe Paziente ha tre campi privati, i suoi getter e setter, un costruttore vuoto e uno che li riceve tutti:
package com.ospedale.prontosoccorso.models;
public class Paziente {
private String nome;
private String cognome;
private int eta;
public Paziente() {
}
public Paziente(String nome, String cognome, int eta) {
this.nome = nome;
this.cognome = cognome;
this.eta = eta;
}
// getter e setter di ogni campo: li genera l'IDE
}Il controller (il nostro PazienteController, raggiungibile su /pazienti) crea un oggetto di quel tipo e lo restituisce. Ogni controller, come sai, si occupa di un solo ambito: i pazienti qui, i medici altrove.
Il traduttore a due direzioni: serializer e deserializer
Dentro Spring Boot lavora un traduttore (sotto il cofano è una libreria, di default Jackson) che conosce due direzioni:
- Serializer: prende un oggetto Java e lo trasforma in JSON. Succede in uscita, quando il controller restituisce qualcosa.
- Deserializer: prende un JSON e lo trasforma in un oggetto Java. Succede in ingresso, quando il client manda dei dati.
È per questo che l'annotazione @RestController è così comoda: dice a Spring che tutto quello che entra ed esce dal controller deve passare da questo traduttore. Tu scrivi Java, il client parla JSON, e in mezzo c'è qualcuno che se ne occupa.
Cosa viene tradotto e cosa passa così com'è?
Il traduttore non scatta sempre, e vale la pena capire quando:
- Una stringa o un numero (un tipo "primitivo") passa così com'è: arriva come testo semplice.
- Un oggetto, come il nostro
Paziente, diventa un documento JSON. - Una lista di oggetti diventa un array JSON: una sequenza di elementi tutti dello stesso tipo, ciascuno con le stesse chiavi.
Nel controller sono due metodi che restituiscono un paziente e una lista di pazienti:
@GetMapping("dettaglio")
public Paziente dettaglio() {
return new Paziente("Mario", "Rossi", 36);
}
@GetMapping("elenco")
public List<Paziente> elenco() {
List<Paziente> elenco = new ArrayList<>();
elenco.add(new Paziente("Mario", "Rossi", 36));
elenco.add(new Paziente("Giulia", "Bianchi", 29));
elenco.add(new Paziente("Luca", "Verdi", 52));
return elenco;
}Aprendo localhost:8080/pazienti/dettaglio il browser mostra il JSON così com'è:

Lo stesso documento, indentato per leggerlo meglio, è questo:
{
"nome": "Mario",
"cognome": "Rossi",
"eta": 36
}
E la lista di pazienti diventa un array, con tre elementi tutti uguali nella forma:

Hai scritto qualcosa per ottenere tutto questo? Niente: crei l'oggetto, lo restituisci, il resto lo fa Spring.
Come fa il client a capire che è JSON?
Se apri la risposta in un client come Postman, accanto al corpo trovi gli header della risposta, e uno si chiama Content-Type. È il server che ti dice cosa c'è dentro il payload:
application/json: il payload è un documento JSON.text/html: il payload è una pagina HTML.text/plain: il payload è testo semplice.
Siccome a tradurre è stato il serializer di Spring Boot, l'header che ti arriva è application/json. Con curl -i lo vedi nero su bianco, e noti anche la differenza con una semplice stringa:
curl -i http://localhost:8080/pazienti/dettaglio
HTTP/1.1 200
Content-Type: application/json
{"nome":"Mario","cognome":"Rossi","eta":36}HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Pronto soccorso operativo
Quell'etichetta non è un optional: serve al client per sapere come leggere quello che ha ricevuto.
Perché non passare tutto con i parametri?
Una domanda lecita: se un paziente ha tre campi, perché non mandarli come parametri nell'indirizzo, come abbiamo fatto prima? Si può fare. Basta un endpoint con tre @RequestParam (nome, cognome, età) che costruisce il Paziente e lo restituisce, e in Postman ti basta aggiungere le tre coppie chiave-valore. Funziona, e Postman ti aiuta pure a compilarle:
@GetMapping("nuova")
public Paziente nuova(@RequestParam String nome,
@RequestParam String cognome,
@RequestParam int eta) {
return new Paziente(nome, cognome, eta);
}{"nome":"Mario","cognome":"Rossi","eta":36}Il problema arriva dopo. Con venti campi l'indirizzo diventa illeggibile, con dati voluminosi non ci stai proprio, e ormai sai cosa succede agli indirizzi: restano nella cronologia e nei log. La POST con il payload esiste proprio per questo.
Fare il percorso inverso: mandare un paziente al server
Finora il server ha solo risposto. Ora lo facciamo ricevere. Il metodo che accetta un oggetto è una POST, e il payload si legge con l'annotazione @RequestBody: dice a Spring "prendi il JSON che c'è nel corpo della richiesta e trasformalo in un Paziente".
Il metodo, nel controller, è questo:
@PostMapping("eco")
public Paziente eco(@RequestBody Paziente paziente) {
return paziente;
}In Postman scegli il verbo POST, apri la scheda Body, seleziona raw e scrivi il JSON:
{
"nome": "Mario",
"cognome": "Rossi",
"eta": 23
}
Con curl la stessa richiesta si scrive così, e la risposta torna indietro identica:
curl -X POST http://localhost:8080/pazienti/eco \
-H "Content-Type: application/json" \
-d '{"nome":"Mario","cognome":"Rossi","eta":23}'{"nome":"Mario","cognome":"Rossi","eta":23}Un metodo di questo tipo, quello che restituisce tale e quale l'oggetto ricevuto, si chiama eco: è il più inutile e il più utile che scriverai. Inutile perché non fa niente. Utile perché, se la risposta ti torna indietro uguale a quello che hai mandato, sai che la richiesta è arrivata, è stata tradotta e il tuo paziente esiste davvero dentro il controller.
Quando qualcosa va storto: due errori da imparare a riconoscere
Il bello di un server che risponde con degli status code è che ti dice anche cosa hai sbagliato. Ce ne sono due che incontrerai quasi subito:
| Codice | Nome | Cosa è successo | Come si risolve |
|---|---|---|---|
| 400 | Bad Request | Hai mandato una richiesta che il metodo non può usare: un parametro mancante, oppure un testo (dieci) dove serviva un numero (10) | Correggi la richiesta |
| 415 | Unsupported Media Type | Hai mandato del JSON, ma senza dire che era JSON: manca l'header Content-Type: application/json | In Postman, nel Body, scegli il formato JSON invece di Text |
Ecco i due errori veri, ottenuti dallo stesso endpoint /pazienti/eco. Prima il JSON mandato senza dichiarare che è JSON (senza l'header Content-Type, curl manda i dati come se fossero un modulo web):
curl -X POST http://localhost:8080/pazienti/eco \
-d '{"nome":"Mario","cognome":"Rossi","eta":23}'{"timestamp":"2026-10-01T17:06:34.004Z","status":415,"error":"Unsupported Media Type","path":"/pazienti/eco"}E poi un testo al posto di un numero, su /pazienti/nuova:
{"timestamp":"2026-10-01T17:06:34.256Z","status":400,"error":"Bad Request","path":"/pazienti/nuova"}La differenza è sottile e istruttiva. Un 400 ti dice che l'endpoint funziona, ma sei tu a chiamarlo male. Un 415 ti dice che il server non ha capito che lingua parli. Se vuoi che ti ascolti in JSON, devi anche avvisarlo che stai parlando in JSON.
E se nel JSON manca un campo? Sul nostro progetto di prova ho provato i tre casi più comuni. Un campo di testo che manca, come nome, non dà errore: arriva semplicemente null.
{"nome":null,"cognome":"Rossi","eta":23}Un campo numerico che manca, come eta, fa invece rispondere 400, perché un numero primitivo non può essere vuoto (lo stesso principio che incontreremo parlando dell'id). E lo stesso accade se sbagli i nomi delle chiavi, per esempio name, surname e age al posto di nome, cognome ed eta: i campi numerici mancano e ricevi un 400. Tutto questo vale per la configurazione di questo progetto, quindi fidati ma verifica sempre sul tuo.
Un'ultima nota, per non farti un'idea sbagliata: in questi esempi non c'è nessuna validazione. Spring ti ferma sui tipi (un numero vuole un numero), ma se mandi un nome o un cognome vuoti li accetta senza battere ciglio:
curl -X POST http://localhost:8080/pazienti/eco \
-H "Content-Type: application/json" \
-d '{"nome":"","cognome":"","eta":23}'{"nome":"","cognome":"","eta":23}In un gestionale vero un paziente senza nome non dovrebbe entrare: controllare che i dati abbiano senso è un lavoro a parte.
Conclusione
Quando un metodo restituisce un oggetto, Spring lo trasforma in JSON. Quando riceve un JSON, con @RequestBody lo trasforma in oggetto. In mezzo, l'etichetta Content-Type fa da cartellino sul pacco e gli status code ti dicono se il pacco è arrivato bene.
Resta una questione scomoda: finora abbiamo sempre restituito pazienti inventati al volo. Nel prossimo pezzo costruiamo qualcosa che somigli a un'API vera: una lista di pazienti da leggere, cercare e aggiungere.

Dario Amato
Sviluppatore software
Informatico con 15 anni di esperienza nello sviluppo software, guidato da una forte curiosità per le nuove frontiere dell'AI e del Web Marketing.
