Salta ai contenuti

JSON con Serde in Rust

Il JSON è ovunque: file di configurazione, API web, salvataggi dei giochi. In Rust quasi tutti usano due crate per lavorarci. serde (abbreviazione di serialize / deserialize, serializzare e deserializzare) sa trasformare i valori Rust in dati e viceversa. serde_json gli insegna il formato JSON.

Pensa a serde come a un traduttore. Descrivi i tuoi dati una volta sola, come una normale struct Rust, e serde li traduce in JSON e ritorno al posto tuo. Non scrivi mai il testo JSON a mano, e non devi mai smontare il JSON carattere per carattere.

Dentro il tuo progetto, aggiungi entrambi i crate. La feature derive permette a serde di scrivere per te il codice di traduzione:

Terminal window
cargo add serde --features derive
cargo add serde_json

Ora il tuo Cargo.toml contiene:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Se Cargo.toml e cargo add ti sono nuovi, leggi prima Cargo e crate. Il Rust Playground ha già entrambi i crate, quindi ogni pulsante «Provalo tu» di questa pagina funziona senza configurare niente.

Aggiungi #[derive(Serialize)] a una struct, e serde_json::to_string può trasformarla in testo JSON:

use serde::Serialize;
#[derive(Serialize)]
struct User {
name: String,
age: u32,
admin: bool,
}
fn main() {
let user = User { name: String::from("Ana"), age: 28, admin: false };
let compact = serde_json::to_string(&user).unwrap();
println!("{}", compact);
let pretty = serde_json::to_string_pretty(&user).unwrap();
println!("{}", pretty);
}

Output:

{"name":"Ana","age":28,"admin":false}
{
"name": "Ana",
"age": 28,
"admin": false
}
▶ Provalo tu

to_string ti dà un JSON compatto, adatto da mandare in rete. to_string_pretty aggiunge a capo e spazi, più comodo per i file che leggeranno delle persone. Le chiavi mantengono lo stesso ordine dei campi della struct.

La direzione opposta si chiama deserializzare. Aggiungi Deserialize all’elenco del derive, poi chiama serde_json::from_str. Gli dici quale tipo vuoi con un’annotazione di tipo:

use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
admin: bool,
}
fn main() -> Result<(), serde_json::Error> {
let text = r#"{"name": "Ana", "age": 28, "admin": false}"#;
let user: User = serde_json::from_str(text)?;
println!("{:?}", user);
println!("{} ha {} anni", user.name, user.age);
Ok(())
}

Output:

User { name: "Ana", age: 28, admin: false }
Ana ha 28 anni
▶ Provalo tu

Due cose da notare:

  • r#"..."# è una stringa raw (grezza). Dentro puoi scrivere " liberamente, cosa comodissima per il JSON.
  • from_str restituisce un Result, perché il testo potrebbe non essere JSON valido. Qui main restituisce Result, quindi possiamo usare ? per passare in alto qualsiasi errore.

Il JSON vero spesso non è come te lo aspetti. Manca un campo, oppure un numero arriva come testo. serde controlla tutto e ti dà un errore chiaro invece di far crollare il programma. Usa match per gestirlo:

use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
let inputs = [
r#"{"name": "Ana", "age": 28}"#,
r#"{"name": "Ben"}"#,
r#"{"name": "Cy", "age": "old"}"#,
];
for text in inputs {
match serde_json::from_str::<User>(text) {
Ok(user) => println!("OK: {:?}", user),
Err(e) => println!("Errore: {}", e),
}
}
}

Output:

OK: User { name: "Ana", age: 28 }
Errore: missing field `age` at line 1 column 15
Errore: invalid type: string "old", expected u32 at line 1 column 27
▶ Provalo tu

I messaggi di serde sono in inglese: il primo dice che manca il campo age (riga 1, colonna 15), il secondo che è arrivata la stringa "old" dove serviva un u32. Il JSON di prova resta in inglese apposta, perché i numeri di colonna dipendono dalla lunghezza del testo. La parte ::<User> si chiama «turbofish». È un altro modo per dire quale tipo vuoi, utile quando non c’è un let user: User su cui mettere l’annotazione.

E se un campo è davvero facoltativo? Hai due buone scelte:

  • Rendi il campo un Option. Una chiave mancante (o null) diventa None.
  • Aggiungi #[serde(default)]. Una chiave mancante riceve il valore predefinito del tipo: 0, false, una String vuota o un Vec vuoto.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Profile {
name: String,
bio: Option<String>,
#[serde(default)]
followers: u32,
#[serde(default)]
tags: Vec<String>,
}
fn main() {
let full = r#"{"name": "Ana", "bio": "Ama Rust", "followers": 120, "tags": ["rust"]}"#;
let short = r#"{"name": "Ben"}"#;
let a: Profile = serde_json::from_str(full).unwrap();
let b: Profile = serde_json::from_str(short).unwrap();
println!("{:?}", a);
println!("{:?}", b);
}

Output:

Profile { name: "Ana", bio: Some("Ama Rust"), followers: 120, tags: ["rust"] }
Profile { name: "Ben", bio: None, followers: 0, tags: [] }
▶ Provalo tu

Usa Option quando «nessun valore» vuol dire qualcosa di diverso da «zero» o «vuoto». Usa default quando il valore predefinito va abbastanza bene.

Rust usa lo snake_case per i nomi dei campi, ma molte API web usano il camelCase, e alcune usano nomi che non sono proprio Rust valido (come type). Non devi rinunciare a dei bei nomi Rust. Usa rename_all per tutta la struct, e rename per un singolo campo:

use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct UserProfile {
user_id: u64,
display_name: String,
is_verified: bool,
#[serde(rename = "type")]
account_type: String,
}
fn main() {
// il tipo di risposta che potrebbe mandare un'API web
let response = r#"{
"userId": 42,
"displayName": "Ana Silva",
"isVerified": true,
"type": "premium"
}"#;
let profile: UserProfile = serde_json::from_str(response).unwrap();
println!("{} (id {}) ha un account {}", profile.display_name, profile.user_id, profile.account_type);
// la serializzazione usa gli stessi nomi, quindi il JSON fa andata e ritorno
println!("{}", serde_json::to_string(&profile).unwrap());
}

Output:

Ana Silva (id 42) ha un account premium
{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}
▶ Provalo tu

Di default serde semplicemente ignora le chiavi JSON che la tua struct non ha. Quindi ti basta descrivere i campi che ti interessano, anche se l’API ne manda cinquanta.

Un array JSON diventa un Vec. Le struct possono anche contenere altre struct e altri vettori, quindi puoi descrivere un JSON profondo semplicemente annidando i tipi:

use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Task {
title: String,
done: bool,
}
fn main() {
let text = r#"[
{"title": "Imparare serde", "done": true},
{"title": "Scrivere un file di configurazione", "done": false},
{"title": "Chiamare un'API", "done": false}
]"#;
let tasks: Vec<Task> = serde_json::from_str(text).unwrap();
for task in &tasks {
let mark = if task.done { "x" } else { " " };
println!("[{}] {}", mark, task.title);
}
let left = tasks.iter().filter(|t| !t.done).count();
println!("{} attività ancora da fare", left);
}

Output:

[x] Imparare serde
[ ] Scrivere un file di configurazione
[ ] Chiamare un'API
2 attività ancora da fare
▶ Provalo tu

A volte non conosci in anticipo la forma del JSON, oppure te ne serve solo un pezzetto. In questi casi usa serde_json::Value. Può contenere qualsiasi JSON: un oggetto, un array, una stringa, un numero, un booleano o null. La macro json! costruisce un Value con una sintassi simile al JSON:

use serde_json::{json, Value};
fn main() {
let data: Value = json!({
"city": "Lisbona",
"temps": [18, 21, 19],
"sunny": true
});
println!("Città: {}", data["city"]);
println!("Prima temperatura: {}", data["temps"][0]);
// as_str() ti dà un semplice &str invece di una stringa JSON
if let Some(city) = data["city"].as_str() {
println!("Testo semplice: {}", city);
}
// una chiave mancante dà Null invece di far crollare il programma
println!("Vento: {}", data["wind"]);
}

Output:

Città: "Lisbona"
Prima temperatura: 18
Testo semplice: Lisbona
Vento: null
▶ Provalo tu

Nota che stampare data["city"] mostra le virgolette: è ancora una stringa JSON. Usa .as_str(), .as_i64() o .as_bool() per ottenere valori Rust semplici. Ognuno restituisce un Option, perché il valore potrebbe essere di un altro tipo.

Value è flessibile, ma perdi l’aiuto del compilatore. Preferisci una struct ogni volta che conosci la forma.

Ecco un esempio dal mondo reale: salvare le impostazioni di un’app in config.json e caricarle di nuovo. Combina questa pagina con quella sui file. std::fs::write salva il testo e std::fs::read_to_string lo ricarica:

use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fs;
#[derive(Serialize, Deserialize, Debug)]
struct Config {
username: String,
theme: String,
font_size: u32,
}
fn main() -> Result<(), Box<dyn Error>> {
let config = Config {
username: String::from("ana"),
theme: String::from("scuro"),
font_size: 14,
};
// salva
let text = serde_json::to_string_pretty(&config)?;
fs::write("config.json", text)?;
// carica
let loaded_text = fs::read_to_string("config.json")?;
let loaded: Config = serde_json::from_str(&loaded_text)?;
println!("Caricato: {:?}", loaded);
Ok(())
}

Output:

Caricato: Config { username: "ana", theme: "scuro", font_size: 14 }
▶ Provalo tu

Perché Box<dyn Error>? Questo main può fallire in due modi diversi: un errore sui file (std::io::Error) o un errore JSON (serde_json::Error). Box<dyn Error> vuol dire «qualsiasi tipo di errore», quindi ? funziona con entrambi. La pagina sugli errori personalizzati lo spiega più nel dettaglio.

Dimenticare di derivare Deserialize. Se la tua struct deriva solo Serialize, puoi scrivere JSON ma non leggerlo. L’errore è lungo, ma le righe importanti sono queste:

error[E0277]: the trait bound `User: serde::Deserialize<'de>` is not satisfied
|
10 | let user: User = serde_json::from_str(r#"{"name":"Ana","age":28}"#).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
|
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `User` type

Il compilatore dice (in inglese) che User non implementa Deserialize e suggerisce di aggiungere #[derive(serde::Deserialize)]. La correzione: #[derive(Serialize, Deserialize)]. Se invece vedi «cannot find derive macro Serialize», hai aggiunto serde senza la feature derive. Esegui cargo add serde --features derive.

Usare .unwrap() su dati che non controlli. Va bene nei piccoli esempi, ma il JSON che arriva da un utente o da un sito può essere rotto. Un file sbagliato farà crollare il programma con un panic. Usa ? o match, come visto sopra.

Aspettarsi che data["name"] venga stampato senza virgolette. Un Value si stampa come JSON. Chiama .as_str() per avere testo semplice.

Crea una struct Item con un name (String) e una qty (u32). Metti due articoli in un Vec, trasforma tutto il vettore in JSON con una sola chiamata e stampalo.

Suggerimento: solo la struct ha bisogno di #[derive(Serialize)]. Anche un Vec di cose serializzabili è serializzabile, e diventa un array JSON.

Mostra la soluzione
use serde::Serialize;
#[derive(Serialize)]
struct Item {
name: String,
qty: u32,
}
fn main() {
let list = vec![
Item { name: String::from("mele"), qty: 3 },
Item { name: String::from("pane"), qty: 1 },
];
let json = serde_json::to_string(&list).unwrap();
println!("{}", json);
}

Output:

[{"name":"mele","qty":3},{"name":"pane","qty":1}]
▶ Provalo tu

serde sa già come gestire Vec, String e i numeri, quindi basta derivare Serialize su Item. Prova to_string_pretty per vedere la stessa lista distribuita su più righe.

Leggi questo JSON in una struct Settings: {"siteTitle": "Il mio blog", "postsPerPage": 5}. La struct deve avere site_title, posts_per_page e un theme che può mancare. Poi leggi anche {"siteTitle": "Appunti"}, dove posts_per_page deve tornare a 0. Stampali entrambi con {:?}.

Suggerimento: ti servono #[serde(rename_all = "camelCase")] sulla struct, Option<String> per theme e #[serde(default)] su posts_per_page.

Mostra la soluzione
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Settings {
site_title: String,
#[serde(default)]
posts_per_page: u32,
theme: Option<String>,
}
fn main() {
let full = r#"{"siteTitle": "Il mio blog", "postsPerPage": 5}"#;
let short = r#"{"siteTitle": "Appunti"}"#;
let a: Settings = serde_json::from_str(full).unwrap();
let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a);
println!("{:?}", b);
}

Output:

Settings { site_title: "Il mio blog", posts_per_page: 5, theme: None }
Settings { site_title: "Appunti", posts_per_page: 0, theme: None }
▶ Provalo tu

rename_all collega siteTitle a site_title e postsPerPage a posts_per_page. Nessuno dei due JSON ha un theme, quindi è None entrambe le volte, e il postsPerPage mancante nel secondo diventa 0. Togli #[serde(default)] e il secondo unwrap() va in panic con «missing field postsPerPage» (campo mancante). Qui .unwrap() va bene perché il JSON l’abbiamo scritto noi; per un input vero, usa ? o match.

  • Aggiungi serde (con --features derive) e serde_json, poi metti #[derive(Serialize, Deserialize)] sulle tue struct.
  • serde_json::to_string / to_string_pretty trasformano un valore in JSON; serde_json::from_str trasforma il JSON in un valore.
  • from_str restituisce un Result. Con un input vero gestiscilo con ? o match, non con .unwrap().
  • Usa Option o #[serde(default)] per le chiavi che possono mancare, e rename / rename_all per far corrispondere i nomi del JSON.
  • Usa serde_json::Value e json! quando non conosci la forma. Quando la conosci, preferisci le struct.

Verifica veloce

1. Quale derive ti permette di trasformare un testo JSON nella tua struct?

2. Un oggetto JSON potrebbe non contenere la chiave bio. Qual è un buon tipo per quel campo?

3. La tua struct ha un campo display_name, ma il JSON usa displayName. Qual è la correzione più semplice per tutta la struct?

4. Cosa fa serde, di default, con le chiavi JSON che la tua struct non ha?

Punteggio: 0 / 4